Filter concepts by levelShowing all levels.

PostgreSQL · Section 1

PostgreSQL Fundamentals

Level
beginner
Read
24 min
Concepts
3

What PostgreSQL actually is — a relational database that enforces the structure you declare, not just a place that accepts SQL — the vocabulary every later section assumes, and how a server, a database, a schema and a connection relate to each other.

This section

What is true here

  1. PostgreSQL checks every declared constraint on every write — a bad row is refused, not stored.
  2. The containment chain is databaseschematablerow/column.
  3. role, extension and tablespace sit outside that chain, cutting across it instead.
  4. One connection is scoped to exactly one database for its whole session.
  5. PostgreSQL is a strong default for durable, relational data — not every workload needs that.

What you will be able to do

  • Explain why PostgreSQL is more than "a place SQL runs" in one sentence
  • Use the fourteen core terms without confusing a view with a materialized view
  • Predict whether a query can reach a given table from a given connection
  • Recognize when Postgres is the right system of record, and when it is not
What a connection can see
hosts manyone connectionsees onegroups

Server (cluster)

one running process

Database

a connection targets exactly one

Schema

a namespace inside the database

Table

rows of typed, constrained columns

  • Server (cluster) — one running process
    • leads to Database (hosts many)
  • Database — a connection targets exactly one
    • leads to Schema (one connection sees one)
  • Schema — a namespace inside the database
    • leads to Table (groups)
  • Table — rows of typed, constrained columns

What PostgreSQL is

The one property that separates a relational database from a place that merely stores JSON.

PostgreSQL as a relational database management system

corebeginner

PostgreSQL is a server that stores data in typed tables and enforces the rules you declare on every write — a NULL, or an order for a nonexistent customer, gets refused. It protects correctness, not just accepts SQL.

Think of it as

A relational database is a strict clerk, not a filing cabinet. A filing cabinet accepts whatever you hand it. A clerk checks the form against the rules before filing it at all — right types, required fields present, references to other files that actually exist — and refuses the ones that fail.

sql
-- The schema is declared once...
CREATE TABLE customers (
    id SERIAL PRIMARY KEY,
    email TEXT NOT NULL UNIQUE
);

-- ...and every later write is checked against it.
INSERT INTO customers (email) VALUES ('a@example.com');   -- accepted
INSERT INTO customers (email) VALUES (NULL);               -- rejected: NOT NULL

What we're doing: Show PostgreSQL refusing writes that violate declared structure, rather than silently storing whatever arrives.

rdbms_demo.sqlsql
CREATE TABLE customers (
    id SERIAL PRIMARY KEY,
    email TEXT NOT NULL UNIQUE
);

CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    customer_id INTEGER NOT NULL REFERENCES customers(id),
    total NUMERIC(10, 2) NOT NULL CHECK (total >= 0)
);

INSERT INTO customers (email) VALUES ('a@example.com');

INSERT INTO orders (customer_id, total) VALUES (999, 50.00);
1–4
Declares the shape once: id, and an email that must be present and unique.
6–10
orders declares its own rules, including a foreign key back to customers and a CHECK that total cannot be negative.
12
A valid customer row — passes NOT NULL and UNIQUE.
14
customer_id 999 does not exist in customers yet, so the foreign key constraint rejects this row outright.
Error
ERROR:  insert or update on table "orders" violates foreign key constraint "orders_customer_id_fkey"
DETAIL:  Key (customer_id)=(999) is not present in table "customers".

Why this works: PostgreSQL checks every constraint declared on a table before a row is allowed to exist — types, NOT NULL, UNIQUE, CHECK and foreign keys alike. The order INSERT never reaches the table; the server rejects it at the boundary. A document store with no schema validation would have stored this row as-is, leaving the application to notice the dangling reference later, if it ever does.

Treating PostgreSQL as "just a place to run SQL"

Wrong

sql
-- No constraints declared — the table accepts anything
CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    customer_id INTEGER,
    total NUMERIC
);

INSERT INTO orders (customer_id, total) VALUES (999, -50.00);

Better

sql
CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    customer_id INTEGER NOT NULL REFERENCES customers(id),
    total NUMERIC(10, 2) NOT NULL CHECK (total >= 0)
);

INSERT INTO orders (customer_id, total) VALUES (999, -50.00);
-- rejected: violates the CHECK constraint on total (checked before the
-- foreign key would even be reached, since customer_id 999 also does not exist)

What you see: The wrong version accepts a negative total and a customer_id that references nothing, silently. Nothing fails until application code (or a person) eventually notices the impossible row.

Why: A table with no constraints is just a typed grid — PostgreSQL will still enforce the column types, but nothing else. The invariants that make the data trustworthy (a total cannot be negative, an order must belong to a real customer) have to be declared explicitly. Skipping them turns a relational database into a fancy, slightly-typed filing cabinet, giving up the exact guarantee an RDBMS exists to provide.

A write passes through the rules before it becomes a row

INSERT arrives

a candidate row

constraints checked

types, NOT NULL, CHECK, foreign keys

row stored

only if every rule passed

  1. INSERT arrives — a candidate row
  2. constraints checked — types, NOT NULL, CHECK, foreign keys
  3. row stored — only if every rule passed

PostgreSQL next to three other stores

PostgreSQL next to three other stores
SystemData modelEnforces structure at write time?
PostgreSQLrelational — typed tables, rows, foreign keysyes, by default
MySQLrelational — typed tables, rows, foreign keysyes, by default
MongoDBdocument — JSON-like documents, flexible shapeonly if you add validation rules
SQL Serverrelational — typed tables, rows, foreign keysyes, by default

Together

sql
CREATE TABLE customers (id SERIAL PRIMARY KEY);

CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    customer_id INTEGER NOT NULL REFERENCES customers(id),
    total NUMERIC(10, 2) NOT NULL CHECK (total >= 0)
);

-- Every row written here is checked against all three rules above,
-- every time, with no extra code required in the application.

Remember: PostgreSQL enforces the constraints you declare on every write — it refuses bad data rather than accepting and storing whatever SQL sends it.

See also: postgresql terminology · server database schema

Advertisement

Vocabulary

The words the rest of the roadmap uses without redefining.

Core PostgreSQL terminology

corebeginner

Fourteen words the roadmap uses without redefining. A database holds schemas, a schema holds tables (and views, sequences, functions...), a table holds rows made of typed columns — roles, extensions and tablespaces sit outside that chain.

Think of it as

Nesting boxes, plus three labels that don't nest. Database contains schema contains table contains row/column — one straight chain you can draw as boxes inside boxes. Role, extension and tablespace are not inside that chain at all: a role can touch objects in any schema, an extension adds objects into a schema, a tablespace is about where bytes live on disk, not about containment.

sql
CREATE SCHEMA sales;
CREATE TABLE sales.orders (id INT, total NUMERIC);   -- table, inside a schema
CREATE VIEW sales.big_orders AS SELECT * FROM sales.orders WHERE total > 1000;
CREATE SEQUENCE sales.order_seq;                     -- generates ordered numbers
CREATE ROLE analyst LOGIN;                           -- an account, or a group

What we're doing: Show the containment chain and two terms that sit outside it — a role and an extension — in one connected script.

terminology_demo.sqlsql
CREATE SCHEMA sales;

CREATE TABLE sales.orders (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    total NUMERIC(10, 2) NOT NULL
);

INSERT INTO sales.orders (total) VALUES (250.00), (1500.00), (75.00);

CREATE VIEW sales.big_orders AS
    SELECT * FROM sales.orders WHERE total > 1000;

SELECT * FROM sales.big_orders;

SELECT table_schema, table_name
FROM information_schema.tables
WHERE table_schema = 'sales';
1
Creates a schema — a namespace, not yet holding any objects.
3–6
The table lives inside sales — its full name is sales.orders.
8
Three rows go in; id fills itself from the identity column's underlying sequence.
10–11
A view: a saved SELECT. big_orders is not a copy of the data — it recomputes the filter on every read.
13
Querying the view reads live from orders through the saved query.
15–17
Both the table and the view show up here — information_schema.tables lists views alongside base tables, and table_schema names the schema explicitly for each.
Output
id | total
----+---------
  2 | 1500.00
(1 row)

table_schema | table_name
--------------+------------
sales         | orders
sales         | big_orders
(2 rows)

Why this works: sales.orders demonstrates schema containing table; id, total and each inserted row demonstrate column and row. big_orders is a view — the same three rows are not duplicated anywhere, the SELECT just runs again each time the view is queried, which is why it only ever shows what currently matches the filter. The information_schema query returns BOTH sales.orders and sales.big_orders — information_schema.tables lists views alongside base tables — which is itself proof that schema is a real, queryable concept in PostgreSQL, not just a naming convention.

Confusing a view with a materialized view

Wrong

sql
CREATE VIEW daily_totals AS
    SELECT date_trunc('day', created_at) AS day, sum(total) AS total
    FROM orders GROUP BY 1;

-- Assuming this view is now a fast, cached snapshot

Better

sql
CREATE MATERIALIZED VIEW daily_totals AS
    SELECT date_trunc('day', created_at) AS day, sum(total) AS total
    FROM orders GROUP BY 1;

-- Explicitly refresh the cached result when it should update
REFRESH MATERIALIZED VIEW daily_totals;

What you see: A plain VIEW used for an expensive aggregate re-runs that full aggregate on every single SELECT against it — there is no caching, so query time does not improve over querying the underlying table directly.

Why: A view is only a saved query text; PostgreSQL substitutes it and re-executes the underlying SELECT every time. A materialized view stores the result physically, so reads are fast — the cost moves to REFRESH MATERIALIZED VIEW, run explicitly or on a schedule, and the data can be stale between refreshes. Picking between them is a real trade-off, not just a naming choice.

The containment chain

Database

one connection targets one database

Schema

a namespace inside the database

Table

rows of typed columns

Row / Column

one record; one typed field

  1. Database — one connection targets one database
  2. Schema — a namespace inside the database
  3. Table — rows of typed columns
  4. Row / Column — one record; one typed field

Fourteen core terms

Fourteen core terms
TermWhat it is
databasea named collection of schemas; one connection targets exactly one
schemaa namespace inside a database that groups tables and other objects
tablea named collection of rows, each with the same typed columns
rowone record in a table
columnone named, typed field every row in a table has
viewa saved SELECT query, queried like a table but computed on every read
materialized viewa view whose result is stored on disk, refreshed on demand
sequencean object that generates a strictly increasing sequence of numbers
functiona stored, named piece of logic that returns a value
procedurea stored, named piece of logic invoked with CALL, not required to return a value
triggera function bound to fire automatically on a table event (INSERT, UPDATE, DELETE)
extensiona packaged bundle of extra types, functions or operators, added to a database
rolean account or a group of privileges — PostgreSQL has one concept for both
tablespacea named location on disk where the objects assigned to it are physically stored

Together

sql
-- schema groups objects inside a database
CREATE SCHEMA sales;

-- table lives in that schema; its columns are typed
CREATE TABLE sales.orders (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,  -- sequence underneath
    customer_id INTEGER NOT NULL,
    total NUMERIC(10, 2) NOT NULL
);

-- view: a saved query over that table, recomputed on every read
CREATE VIEW sales.big_orders AS
    SELECT * FROM sales.orders WHERE total > 1000;

-- role: an account that can be granted privileges on any of the above
CREATE ROLE analyst LOGIN;
GRANT SELECT ON sales.big_orders TO analyst;

Remember: database → schema → table → row/column is the containment chain. Role, extension and tablespace answer a different question and sit outside it.

See also: postgresql as rdbms · server database schema

Advertisement

Scope and fit

What one connection can see, and when PostgreSQL is the right tool at all.

Server, database, schema and connection

standardbeginner

A PostgreSQL server (a "cluster") can host several databases at once, but one connection always targets exactly one of them for its whole session — reaching a second database needs a second connection, not a different table name.

Think of it as

A PostgreSQL server (a "cluster" in Postgres's own terms) is one running process that can host several databases side by side, the way one office building can host several separate companies. A connection is a phone line to exactly one company's floor — you cannot casually wander into another company's floor over that same line. Querying a second database means opening a second connection, not just writing a different table name.

sql
-- psql connects to exactly one database per invocation
psql -h localhost -U app_user -d storefront

-- inside that connection, schema-qualify to be explicit
SELECT * FROM public.orders;
SELECT * FROM reporting.daily_totals;

-- to reach a DIFFERENT database on the same server, open a new connection
psql -h localhost -U app_user -d analytics

What we're doing: Show that a connection to one database cannot query a table in a sibling database on the same server, and that schemas inside one database are reachable without reconnecting.

connection_scope.sqlsql
-- Run from a connection to database "storefront"
CREATE SCHEMA reporting;
CREATE TABLE reporting.daily_totals (day DATE, total NUMERIC);

-- Reachable: a different schema, same database, same connection
SELECT count(*) FROM reporting.daily_totals;

-- Not reachable from this connection at all: a table in database "analytics",
-- named the full three-part way (database.schema.table)
SELECT count(*) FROM analytics.public.events;
2
A new schema inside the current database — no new connection needed.
6
Cross-schema, same-database access works over the one open connection.
10
analytics is a different database. Naming it explicitly with a three-part reference is what surfaces PostgreSQL's actual error — this connection cannot be redirected mid-session to look there.
Error
ERROR:  cross-database references are not implemented: "analytics.public.events"

Why this works: PostgreSQL enforces database isolation at the connection level: a session authenticates against one database name and stays scoped to it for its whole lifetime. Schemas are a lighter-weight namespace INSIDE that one database, so reporting.daily_totals is one query away. A two-part name like analytics.events does not even get this far — since analytics is not a schema in the current database, PostgreSQL reports "relation does not exist" instead, which is a different (and more confusing) error. Writing the full three-part database.schema.table form is what surfaces the real reason: there is no SQL syntax that crosses the database boundary the way schema-qualifying a table name crosses a schema boundary.

Assuming a schema-qualified name can cross a database boundary

Wrong

sql
-- Connected to "storefront" only, trying to reach another database
-- the same way a schema is reached
SELECT * FROM analytics.public.events;

Better

bash
# Open a separate connection scoped to the other database
psql -h localhost -U app_user -d analytics -c "SELECT * FROM events;"

What you see: ERROR: cross-database references are not implemented — raised immediately, not a permissions error and not something a GRANT can fix.

Why: schema.table syntax only ever resolves inside the database the connection is already scoped to. A second database on the same server is invisible to that syntax no matter what privileges the role holds — it requires a distinct connection (dblink and postgres_fdw exist specifically to bridge this, at real added complexity, which is itself evidence that it isn't native SQL syntax).

Remember: A connection is scoped to exactly one database for its whole session — schema-qualifying crosses a schema, nothing in plain SQL crosses a database.

See also: postgresql as rdbms · postgresql terminology

Advertisement