You do not need relational algebra to ship a product. You need to know what a table is, how two tables point at each other, and what happens when you change the shape of one that already holds real customer records. That is roughly a page of material, and skipping it is how people end up with a live database nobody dares touch.
The idea
A database is a set of tables. A table is one kind of thing: users, orders, posts. Its columns are the fields every one of those things has, and each column has a type, so a price column holds numbers and refuses the word "free". A row is one actual thing: one user, one order. Tables point at each other by storing the other row's id, and every structural change you make gets written down as a A small file of database instructions that changes the shape of your tables. You run it once, and it runs the same way everywhere. so your laptop, your test copy and production all end up identical.
users orders
+----+------------------+ +----+---------+-------+-----------+
| id | email | | id | user_id | cents | status |
+----+------------------+ +----+---------+-------+-----------+
| 1 | [email protected] |<---+ | 11 | 1 | 4000 | paid |
| 2 | [email protected] |<-+ | | 12 | 1 | 2000 | refunded |
+----+------------------+ | +-----| 13 | 2 | 4000 | paid |
^ +-------| | | | |
| +----+---------+-------+-----------+
primary key: unique orders.user_id holds a users.id
id for one row = a foreign key. One user, many orders.How it works
The id column is the primary key, a value no two rows share. When orders.user_id stores 1,
the database can enforce that a row with id 1 exists in users, and it will refuse an order for a
user who does not. That refusal is the point. It is cheaper than discovering six months of orphaned
rows.
You ask questions in SQL: select * from orders where user_id = 1 and status = 'paid'. If that
question gets slow, you add an A lookup shortcut you add to a column so the
database can find matching rows without reading every row. on the column you filter by.
Changing structure works differently from changing data. Adding a status column is a migration: a
numbered file such as 0003_add_order_status.sql that lives in git, runs once per environment, and
is never edited after it has run anywhere. If it was wrong, you write 0004 to correct it. Tools
like the Supabase CLI, Prisma and Rails do the bookkeeping of which files have already run.
What to do
- Sketch your tables before you ask an AI to build them. Names, columns, and which table points at which. Five minutes here saves a rebuild later.
- Store money as whole cents in an integer column and timestamps in UTC. Both mistakes are painful to undo once real rows exist.
- Never open the production table editor and fix a row by hand. Do it as a written statement or a migration you can read back, because a hand edit leaves no record and nobody can reproduce it on the copy running on your laptop.
- Take a backup before running a migration against real data, and know how you would restore it.
Where it breaks
Migrations are forward-only in practice. Dropping a column is instant and unrecoverable, and "undo" means restoring a backup, so treat a destructive migration as a separate, deliberate piece of work. Foreign keys also cause deletes to fail, which reads as a bug and is usually the database correctly refusing to strand rows. And an AI assistant will happily generate a schema that looks professional and models the wrong thing. It cannot know that your orders can have two payers. You can.