Score 100% on the quiz to continue
The shape of a database
Tables, rows, columns, primary and foreign keys, and how to read a schema you did not write.
Every query you will ever write rests on one mental model. Once you have it, SQL stops being a magic incantation you copy from a developer and becomes something you can reason about. This lesson builds that model, and it is the only lesson in the course with more reading than typing.
Tables, rows, and columns
A relational database stores data in tables. A table is a grid:
- a column is a field, with a name and a type, the same for every entry
- a row is one entry, one thing: one post, one category, one tag
Our blog has four tables. Look at categories, the simplest one:
| id | name | slug | created_at |
|---|---|---|---|
| 1 | Testing | testing | 2026-08-06 10:12:44 |
| 2 | Databases | databases | 2026-08-06 10:12:44 |
| 3 | Automation | automation | 2026-08-06 10:12:44 |
Three rows, four columns. A spreadsheet with rules, and the rules are the interesting part.
The primary key
Look at the id column. It is the primary key: the value that identifies a row uniquely and never repeats within the table. Two categories can never share an id.
In this schema the primary keys are SERIAL, which means the database assigns the next number itself whenever a row is inserted. You do not choose it, and you cannot rely on it being sequential after deletions, but within a table it is guaranteed unique.
👨🏫 As a tester, the primary key is your row's fingerprint (). When you find a bad row, note its id. That is the one piece of information that stays true no matter how the record is later edited, and it is what turns "one of the posts is wrong" into "post 14 is wrong", which is something a developer can act on.The foreign key, and the one-to-many shape
Now open posts:
\d postsAmong its columns is category_id, and at the bottom of the output you see this:
Foreign-key constraints:
"posts_category_id_fkey" FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULLA foreign key is a column that points at the primary key of another table. posts.category_id holds the id of a row in categories. That is how the two tables are connected, and it gives us a one-to-many relationship: one category has many posts, one post has at most one category.
The foreign key is also a rule the database enforces. You cannot store category_id = 999 if no category 999 exists; the database rejects the row. That is a guarantee about your data that no amount of application code can be trusted to provide on its own, and it is the reason a tester should read the schema before writing the first test.
Two details in that constraint matter later:
category_idis nullable. A post is allowed to have no category at all. That single fact is behind a whole family of testing mistakes, and it gets a lesson of its own.ON DELETE SET NULL. If a category is deleted, its posts survive, withcategory_idset toNULL. The database decided what happens, not the application.
The join table, and the many-to-many shape
A post can have several tags, and a tag can be on several posts. That is many-to-many, and there is no way to express it with a column on either side. Which column would hold three values?
So a relational schema uses a fourth table, post_tags:
\d post_tags| post_id | tag_id |
|---|---|
| 1 | 1 |
| 1 | 4 |
| 1 | 6 |
| 2 | 2 |
| 2 | 1 |
Two columns, both foreign keys, and together they form the primary key. Each row means "this post has this tag". A post with three tags produces three rows here. That is all a join table is: a list of pairs.
Two consequences worth carrying with you:
- Because
(post_id, tag_id)is the primary key, the same tag cannot be attached to the same post twice. The database makes the duplicate impossible. - Both foreign keys are
ON DELETE CASCADE, so deleting a post automatically removes its rows here. The tags themselves survive, because other posts may still use them.
👨🏫 Whenever you see a table whose name looks like two other tables glued together (post_tags,order_items,user_roles), you are looking at a many-to-many relationship. Nine times out of ten it is also where the interesting bugs are, because it is the only part of the schema the application has to maintain by hand.
Reading a schema you did not write
You have three ways to learn a schema, and a good tester uses all three.
1. The schema file. In this project, db/schema.sql is 40 lines and tells you everything: the tables, the types, what is NOT NULL, what is UNIQUE, the foreign keys, and the indexes. When a project has one, read it first.
2. The database itself. \dt lists the tables, \d <table> describes one. This works on any database, including a production one where you have never seen the source code, which is exactly when you need it most.
3. The picture. For four tables, a sketch beats both:
categories posts tags
---------- ----- ----
id PK <-------- category_id FK id PK
name UNIQUE id PK name UNIQUE
slug UNIQUE title slug UNIQUE
created_at body created_at
created_at
\ /
\ post_tags /
-- post_id FK --
tag_id FKArrows point from the foreign key to the primary key it references. When you can draw that for an application, you can query it.
What the schema tells a tester before any test runs
Reading a schema is a testing activity in itself. Every constraint answers a question you would otherwise have to ask a developer, and every missing constraint is a test case:
| What you see | What it tells you |
|---|---|
NOT NULL on title | A post without a title is impossible. No need to test it at the database level, and if the form allows it, the request will fail. |
UNIQUE on categories.name | Two categories cannot share a name. Worth checking what the interface does when you try. |
category_id nullable | A post with no category is a valid state. Every listing, filter, and report has to cope with it. |
ON DELETE CASCADE on post_tags | Deleting a post cleans up after itself. Worth confirming that it really happens. |
No UNIQUE on posts.title | Two posts may share a title. If someone reports that as a bug, the schema says it is by design. |
👨🏫 That last row is the one I want you to remember. Half of "is this a bug?" arguments are settled in thirty seconds by reading the schema.
Commands overview 📖
\dt
Lists the tables in the current database.
Syntax:
\dtExample:
\dt\d
Describes a table: columns, types, nullability, defaults, indexes, and foreign keys.
Syntax:
\d <table-name>Example:
\d post_tagsSuggested content 📚
- Data Definition - official PostgreSQL documentation
- Constraints - official PostgreSQL documentation
- Foreign keys - official PostgreSQL documentation
Exercise 🎯
Without running a single SELECT, use \d on all four tables and answer these, writing your answers down before you check the spoiler:
- Which columns in the whole schema are
UNIQUE? - If someone deletes the
Databasescategory, what happens to its posts? - If someone deletes a post that has three tags, how many rows disappear from
post_tags, and how many fromtags? - Can two different posts have exactly the same title?
🙊 Here are the answers:
1.categories.name,categories.slug,tags.name, andtags.slug. Plus the primary keys, which are unique by definition:categories.id,posts.id,tags.id, and the pair(post_id, tag_id)inpost_tags.
2. Nothing is deleted.ON DELETE SET NULLmeans the posts survive withcategory_idset toNULL. They then appear as posts with no category.
3. Three rows disappear frompost_tags, because ofON DELETE CASCADE. Zero rows disappear fromtags: the tags themselves belong to the whole blog, not to that post, and other posts may still be using them. Finding the tags that no longer belong to any post is a fine exercise, and we do exactly that two lessons from now.
4. Yes. There is noUNIQUEconstraint onposts.title, so duplicate titles are allowed by design. We will create a pair of them on purpose later, and then find them with a query.
Show the world what you learned 🌎
To show your professional network what you learned in this lesson, post the following on LinkedIn.
I am taking the "SQL for Testers" course by @Walmyr Lima e Silva Filho at the @Talking About Testing School, where I learned to read a database schema like a tester: primary keys, foreign keys, one-to-many and many-to-many relationships, and what every constraint tells you about the application before a single test runs. #TalkingAboutTesting #TATSchool #SQLForTesters #SQL #PostgreSQL #Testing
👨🏫 Remember to tag me in your post. Here is my LinkedIn profile.
Quiz
Why does a many-to-many relationship between posts and tags need a fourth table, while the one-to-many between posts and categories needs only a column?
Score 100% on the quiz to continue