Every page is a question asked of the database
The screen is the answer. The query is the question, and it is where the two failures that matter most both live: the wrong rows, and somebody else’s rows.
You do not need to write SQL fluently. You need to read one query and say what it will return.
The shape of a select
SELECT id, starts_at, status
FROM bookings
WHERE customer_id = 71
AND status = 'confirmed'
ORDER BY starts_at DESC
LIMIT 20;Read it in the order the database does. FROM names the table. WHERE decides which rows survive. ORDER BY sorts them. LIMIT cuts the list. SELECT picks which columns come back.
The WHERE clause is the whole security story. Remove customer_id = 71 and this returns every confirmed booking belonging to everybody.
Joins, briefly
A booking holds a customer_id, not a customer name. Getting the name means joining:
SELECT b.starts_at, c.name
FROM bookings b
JOIN customers c ON c.id = b.customer_id
WHERE b.stylist_id = 4;JOIN keeps rows that match on both sides. LEFT JOIN keeps every row from the left table even when the right has no match, filling the missing columns with NULL. That difference produces a real bug: a plain JOIN between bookings and customers silently drops every booking whose customer was deleted, and the count on your dashboard is wrong.
NULL is not empty
NULL means unknown, and it does not behave like a value.
WHERE cancelled_at != '2026-01-01' -- excludes rows where cancelled_at IS NULL
WHERE phone = NULL -- never true, for any row
WHERE phone IS NULL -- the correct formThe first line is the trap. A filter meant to exclude one date also silently excludes every row where the column was never set. This is exactly the class of bug that hides a subset of rows from a list without any error appearing anywhere, and it is common in generated queries.
Reading an ORM instead
Most generated projects use a query builder, which is the same thing with different punctuation:
const rows = await db
.select()
.from(bookings)
.where(and(
eq(bookings.customerId, session.userId),
eq(bookings.status, 'confirmed')
))
.orderBy(desc(bookings.startsAt))
.limit(20)Same five clauses. eq is =, and is AND, desc is DESC. The important habit transfers exactly: find the where, and check that the user’s identity in it came from the session and not from the request.
If you cannot tell what SQL a builder produces, most of them will print it. In Drizzle, .toSQL(). In Prisma, set the query log. Seeing the real statement once removes a lot of uncertainty.
Three things to check in any query you review
Is it scoped to a person? Any query returning user data needs a condition tying it to the logged-in identity, taken from the session on the server. This is the single most common serious hole in AI-built apps and it has a whole lesson later.
Is there a LIMIT? A list page with no limit works on your 40 test rows and takes eleven seconds when a real account has 90,000.
Is it inside a loop? Fetching a list and then querying once per item is the N+1 problem: 1 query becomes 201. It is invisible locally and it is the most common reason a generated page is slow.
Look at the data yourself
The fastest way to check a page is to ask the database the same question directly.
psql $DATABASE_URL # Postgres, free, comes with it
sqlite3 app.db # SQLite, one commandOr use a free graphical client: DBeaver, pgAdmin, or the viewer built into Neon and Supabase. Type the query, count the rows, compare with the screen. When the two disagree, you have located the problem to one side of the line, and that is most of the work.
This single habit, looking at the actual rows, changes your relationship with an app you did not write more than any other in this block. The screen can be wrong in a hundred ways. The table cannot argue with you.
Every lesson here is open. Your progress is recorded once you pass the Building with AI exam.
Check your understanding
A dashboard’s booking count is lower than the number of rows in the table. The query uses JOIN customers. What should you check first?
Pick the one you would defend. Your answer stays private.
Stuck on something?Ask Beatriz, your AI tutor