Part · Chapter

Relational databases, refreshed

A fast refresher on the vocabulary: tables, keys, relationships, NULL, and the words MySQL and PostgreSQL use differently.

The short version

  • A table is a set of rows. Same columns, one value per cell, no built-in order.
  • Keys identify and connect. A primary key names a row, a foreign key points at one.
  • Foreign keys refuse bad data. No orphan rows, and no cascade unless you asked.
  • NULL means no value here. Test it with IS NULL, never =.
  • "Schema" depends on the engine. A database in MySQL, a namespace inside one in PostgreSQL.

The relational model in plain words

A DBMS gives a business what a file cannot: shared access, concurrency, durability and enforced rules (see Transactions and concurrency). Codd's 1970 model gave it this vocabulary, formal word then everyday one.

Relation (table)
A named set of rows with the same columns.
Tuple (row, record)
One entry: (1, 'Amira', 'Dubai').
Attribute (column, field)
A named, typed slot every row has (types).
Schema
The structure: tables, columns, types, keys, constraints. It changes only in a migration.

Three rules still matter. One value per cell, so a film's five lead actors never go in one cast column, they go in a table of their own (chapter 3 builds it). Rows have no inherent order, so only ORDER BY guarantees one. Rows are identified by their values, hence keys.

Both engines expose the schema through INFORMATION_SCHEMA views; PostgreSQL also has pg_catalog.

Remember

A table is a set of rows with identical columns, one value per cell and no inherent order.

Keys: how rows are identified and connected

A key is a column, or group of columns, whose values identify a row.

customers
idnamecity
1AmiraDubai
2BenCairo
3ChloéNULL
4DevRiyadh
orders
idcustomer_idorder_datestatustotal
1012026-01-05paid65.00
1112026-02-11paid180.00
1222026-02-20pending40.00
13NULL2026-03-01paid20.00
1432026-03-15cancelled45.00

Primary, candidate, natural and surrogate

  • Primary key. Unique, never NULL, at most one per table: customers.id.
  • Candidate key. Any other set that could have served, such as a unique email. Declare those UNIQUE.
  • Natural versus surrogate. An email or ISBN comes from the business and can change; a handed-out number does not. MySQL uses AUTO_INCREMENT, PostgreSQL an identity column.
  • Composite key. Spans columns: order_items is keyed by (order_id, product_id).

Foreign keys and referential integrity

A foreign key holds another table's primary key value. The engine then refuses a child row with no parent, and refuses to delete a parent with children unless you named an action: RESTRICT or NO ACTION (refuse, the default), CASCADE (delete the children) or SET NULL (blank the pointer).

Illustration · MySQL, shortened
CREATE TABLE orders (
  id          INT AUTO_INCREMENT PRIMARY KEY,
  customer_id INT NULL,
  CONSTRAINT fk_orders_customer FOREIGN KEY (customer_id)
    REFERENCES customers(id) ON DELETE SET NULL
) ENGINE=InnoDB;
CREATE TABLE order_items (
  PRIMARY KEY (order_id, product_id),
  CONSTRAINT fk_items_order   FOREIGN KEY (order_id)   REFERENCES orders(id) ON DELETE CASCADE,
  CONSTRAINT fk_items_product FOREIGN KEY (product_id) REFERENCES products(id)
);

Delete Amira and orders 10 and 11 stay with customer_id NULL, like guest order 13. Delete order 10 and its order_items rows go too.

Illustration · Both
INSERT INTO orders (customer_id, order_date) VALUES (99, '2026-04-01');
-- MySQL:      ERROR 1452 (23000): Cannot add or update a child row:
--             a foreign key constraint fails (...fk_orders_customer...)
-- PostgreSQL: ERROR: insert or update on table "orders" violates
--             foreign key constraint "fk_orders_customer"  (SQLSTATE 23503)

DELETE FROM products WHERE id = 101;
-- MySQL:      ERROR 1451 (23000): Cannot delete or update a parent row:
--             a foreign key constraint fails (...fk_items_product...)
-- PostgreSQL: ERROR: update or delete on table "products" violates
--             foreign key constraint "fk_items_product" on table "order_items"

Those errors are good news. The same family covers UNIQUE (MySQL 1062, PostgreSQL SQLSTATE 23505), NOT NULL and CHECK, enforced in MySQL since 8.0.16. More: Designing good tables.

For you as a DBA

MySQL Only InnoDB enforces foreign keys, and a table with no primary key falls back to a hidden row ID that slows replication. PostgreSQL can mark a constraint DEFERRABLE so the check waits until commit.

Careful

ON DELETE CASCADE is convenient in development and terrifying in production: one parent DELETE can silently remove thousands of child rows. Check the largest possible parent delete, and that backups restore.

Ask the AI

Do not memorize the catalog views. Ask: "MySQL 8.4: list every foreign key that references customers, with child table, column and ON DELETE action, from INFORMATION_SCHEMA." Swap in "PostgreSQL 18" for pg_catalog.

Relationships at a glance

There are only three shapes, and the shape says where the foreign key goes.

ShapeWhere the keys goIn the shop
One-to-oneEach row matches at most one row on the other side. Often really one table, split because part of it is optional or sensitiveA customer and one loyalty profile
One-to-manyThe foreign key always lives on the "many" sideorders.customer_id, never a list of order ids in customers
Many-to-manyNeither table can hold the other's key, so a junction table sits between themorder_items, which also carries quantity and the price at the time of sale
customers id (PK) name, city orders id (PK) customer_id (FK) order_items PK: order_id, product_id both are FKs products id (PK) name, price 1 N 1 N 1 N Each arrow points from the "1" side (the primary key) to the "N" side (the foreign key).
Two one-to-many relationships meet in the junction table order_items.

Normalization is in Designing good tables; putting the tables back together is Joins.

NULL: the value that is not a value

NULL marks the absence of a value, not zero and not an empty string. Chloé's city is NULL because it is unknown; order 13's customer_id is NULL because a guest checkout has no customer.

SQL uses three-valued logic: any comparison with NULL is UNKNOWN, even NULL = NULL. WHERE keeps only rows that are TRUE, so city = NULL keeps nothing and city <> 'Dubai' silently drops Chloé. Test with IS NULL.

Illustration · Both
SELECT name FROM customers WHERE city = NULL;    -- 0 rows, always
SELECT name FROM customers WHERE city IS NULL;   -- Chloé

-- Count rows, versus count non-NULL values in a column
SELECT COUNT(*) AS rows_total, COUNT(city) AS cities_known
FROM customers;
rows_totalcities_known
43

COUNT(*) counts rows; COUNT(city) counts values, and NULL is not one. SUM, AVG, MIN and MAX ignore NULLs too (see Aggregation and window functions). For NULL-safe equality, MySQL has <=> and PostgreSQL IS NOT DISTINCT FROM; both have COALESCE().

The words that trip returning people

A server is the machine or the program on it, so ask which. An instance is one running copy, with its own configuration file, data directory and port.

LevelMySQL 8.4 / 9.7 / 26.xPostgreSQL 17 / 18
One running copyAn instanceA cluster: one data directory, one port, nothing to do with several machines
Inside itDatabases, each a folder in the data directoryDatabases, each holding schemas, which hold the tables
"Schema" meansA database: CREATE SCHEMA is a synonym for CREATE DATABASEA namespace inside a database; every database starts with public
Reach another databaseYes, as shop.customers, with rightsNo, a connection is tied to one database (needs postgres_fdw)

So "a separate schema" is a namespace inside one database in PostgreSQL, and a separate database in MySQL. PostgreSQL resolves unqualified names through search_path, and since version 15 creating in public needs a grant (Users, roles and security).

Client and server

The server is one process (mysqld.exe or postgres.exe) running as a Windows service on a TCP port; a client sends SQL text and gets rows back. See Clients and tools and The SQL map.

MySQL classic port

3306

MySQL X Protocol port

33060

PostgreSQL port

5432

PowerShell
# The server is just a Windows service; names come from the installer
Get-Service MySQL84, postgresql-x64-18

# Who is listening on the MySQL port?
netstat -ano | findstr 3306

What a DBA owns, and what developers own

Developers own the queries and most of the schema design. You review and protect.

AreaThe DBAThe developer
AvailabilityKeeps the service up, plans replicas and failover, takes the 3 a.m. pageMakes the app reconnect and retry
Backups and recoverySchedules backups, tests restores, knows the recovery objectivesSays how much data loss the business tolerates
SecurityCreates accounts, grants least privilege, encrypts connections, patchesRequests minimum access, never uses root
PerformanceWatches slow queries, memory and locks; suggests indexes and settingsWrites the queries, fixes the flagged ones
CapacityTracks disk, memory, connections and growthWarns about features that add load or data
Schema changesReviews migrations for keys, constraints, locking and rollbackDesigns tables, writes the migrations
UpgradesPlans quarterly patches and stepwise major upgradesTests the app against the new version

Each row has its own chapter, from Users, roles and security to A DBA's working week. The posture: ask for the foreign keys and the rollback plan before saying yes.

Going deeper

Quick check

1. You run SELECT * FROM orders twice and the rows come back in a different order the second time. What happened?

A table is a set. The engine returns rows in whatever order is convenient, and that can change with indexes, rebuilds or the plan it picks. Reread "The relational model in plain words".

2. orders.customer_id is declared with ON DELETE SET NULL. What happens when you delete customer 1 (Amira)?

SET NULL keeps the child rows and blanks the pointer. CASCADE would delete them; RESTRICT or NO ACTION would refuse. Reread "Foreign keys and referential integrity".

3. On the shop's customers table, what do COUNT(*) and COUNT(city) return?

COUNT(*) counts rows (four customers). COUNT(city) counts non-NULL values, and Chloé's city is NULL, so it returns three. Reread "NULL: the value that is not a value".

4. A colleague runs CREATE SCHEMA reporting; on your MySQL 8.4 server. What did they create?

MySQL treats CREATE SCHEMA as a synonym for CREATE DATABASE, so a new database folder appears in the data directory. PostgreSQL is the engine with schemas inside a database. Reread "The words that trip returning people".

5. Which shop table is the junction table that makes the many-to-many relationship between orders and products possible?

order_items holds one foreign key to orders and one to products, with a composite primary key over both, plus its own facts (qty, unit_price). Reread "Relationships at a glance".