The project is done: the schema is created, the data loads, the fifteen queries return what they should and the indexes are justified. And even so, half the work is still missing, because there's one thing almost nobody teaches and it decides how everything above gets judged: telling the story of it. An excellent model badly explained is marked like a mediocre model; a correct number presented without its definition reads like a wrong number; and a repository nobody manages to run in five minutes is, in practice, an empty repository.
This lesson is that other half: the report section by section, how data results are presented without misleading in good faith, the defence questions and how to prepare them, how to publish the project so somebody can run it, and the self-assessment with 12-02's rubric turned into a checklist. And after that, the close of the whole course.
Contents
- The report:
04-report.md - How to present data results
- The defence: the questions you'll be asked
- Publishing the project
- Self-assessment: the rubric as a checklist
- Common Mistakes and Tips
- Exercises
- Course conclusion
- The report:
04-report.md
04-report.mdThe report isn't technical documentation —that's what the DDL comments are for— nor a diary of what you did. It's a document that answers three questions: what you've built, why like that and what you know isn't right yet. Between 4 and 8 pages; more than that doesn't get read, and less than that usually means there are no decisions being told.
| Section | Length | What goes in it |
|---|---|---|
| 1. Context and scope | ½ page | The brief in five lines, and above all what's out of scope (12-01, section 6). A reader who doesn't know what you haven't done will demand what you never promised |
| 2. Data model | 1-1½ pages | The ER diagram, the list of tables with one line each, and the paragraph explaining work versus copy. If only one section gets read, it'll be this one |
| 3. Design decisions | 1½-2 pages | The heart of the report: 6-8 decisions, each with the alternative you discarded and why. With no alternative, it isn't a decision: it's the first thing that occurred to you |
| 4. Featured queries | 1-1½ pages | Three or four, not fifteen. The most useful for the business, the technically hardest and the one that gave you most trouble. With their result and their reading |
| 5. Performance | ½-1 page | The list of indexes with the query that justifies each one, and a before/after EXPLAIN with real volume |
| 6. Security and personal data | ½ page | The three roles, the has_table_privilege that proves it, and how account closure and anonymisation are handled |
| 7. Limitations and future work | ½ page | What you know is missing. It's the section that raises the mark most and the one most people leave out |
| Appendix | — | How to run it, and the table of load counts |
On tone
- Write in the present tense and in the first person plural or impersonally: "the loan hangs off
copiesbecause…", not "I decided that…" or "it could have been done…". Firm, without arrogance. - One decision, one paragraph, one why. The pattern that works is always the same: what I did · what alternative there was · why this one · what it costs me. That fourth element —the price you pay— is what distinguishes somebody who has decided from somebody who got it right by accident.
- No adjectives without data. "Performance is excellent" says nothing; "the overdue query goes from 340 ms to 4 ms with the partial index, over 50,000 loans" does.
- Not one screenshot of code. Text blocks, which can be copied, searched and read on any screen.
Section 2: how to show a model
The ER diagram doesn't speak for itself. A reader looking at twelve boxes and sixteen arrows understands nothing unless you tell them where to start. The sequence that works, and it fits in one paragraph under the diagram:
- The backbone first. "The system revolves around
loans: one row per copy lent to a member." One sentence, and the reader already knows where to look. - The twist, with a concrete example. "
worksholds the title;copies, each physical volume. There are three copies of The Garden of Hours across two branches." An example with proper names is worth more than three paragraphs of theory. - The two relationships that need a second look. The N:M
works_authorsand the self-referencing one onlibrarians. - The satellite tables in one line each.
fines,reservations,subjects,publishers: they need no more.
And one formatting decision: the diagram goes in as an image, not as code. Export the mermaid to PNG or SVG and leave it in report/er-model.png. Whoever reads the report in any Markdown viewer will see it; whoever reads the source will too.
Section 4: how to present a query
Four elements, always in the same order, and one query per page:
The question — which copies have been out past their date the longest, and whose are they?
The definition — an overdue loan = with no
return_dateand with adue_dateearlier than today. The fine doesn't exist yet (BR-10): the column is an estimate.The query and its result — the
v_overdue_loansview, with the three rows: 36 days and €7.20 for Lena Fuentes, and two one-day loans.The reading — three overdue loans out of six active ones is a high rate, but two of the three are a single day over: the real problem is one loan, and it's more than a month old.
That fourth element is the one almost nobody writes and the only one that interests whoever has to decide. A table with no reading forces the reader to do your job; and if they do it, they'll draw their own conclusion, which may not be the right one.
Section 7, the one that raises the mark
Acknowledging the limits doesn't subtract, it adds, because it shows you understand the system better than somebody who thinks they've closed it. For this project, five honest, well-chosen limitations:
- The partial index prevents two active loans, not two overlapping historical loans; the solution would be
EXCLUDEwithbtree_gist(12-04). - The member's status (
suspended) is maintained outside the database: nothing guarantees it matches their real debt. It would take a scheduled nightly job or a trigger onfines. - There's no audit trail: if somebody changes a
return_dateby hand, no trace is left. - The reservation queue doesn't allow for priorities, only pure FIFO, and the 3-day expiry doesn't apply itself.
- The system is for one network, not multi-institution: there's no interlibrary loan and no organisations table.
- How to present data results
All of this is 11-04 applied to your own report, and it's what separates a table from an argument.
Define the metric before you show it, always. No exceptions. In this project there are three pairs of definitions that are easily confused and have to be pinned down in writing:
| Metric | Project definition | What it gets confused with |
|---|---|---|
| Loans | Rows of loans, open and closed, all members |
Only the returned ones, which gives a smaller figure and is no good for measuring demand |
| Outstanding debt | SUM(amount) of fines with no paid_date: €13.40 |
The potential debt of overdue loans not yet returned: €7.60 more. They're two figures and two names |
| Available work | With at least one enabled copy that isn't on loan | With at least one copy, full stop — which counts the ones in repair |
Table or chart. The practical rule: a table when the reader is going to read specific values or there are fewer than a dozen rows; a chart when what matters is the shape —a trend, a comparison of magnitudes, a distribution—. QR-12's monthly series calls for a chart; QR-15's branch × subject pivot, with its three rows, calls for a table. And the pivot never a pie chart with six slices, which is the most effective way of making sure nobody compares anything.
Every number needs its denominator. "6 fines" means nothing; "6 fines out of 30 returned loans, 20.0 %" does. And the other way round: a percentage without the absolute figure is just as misleading. The project's rule, which comes straight from 11-04: if the denominator drops below a few dozen, publish the absolute number and not the percentage. Saying the Biblioteca Infantil has "100 % children's loans" is true and it's noise: it's 6 loans.
Rounding and units. Round only when presenting, never in the intermediate steps. Money, to two decimals and with the symbol (€7.20); days, whole numbers; averages, to one decimal (32.3 days) because the second decimal is false precision with 36 rows. And the unit in the column header, not repeated in every cell.
Honesty about the sample size. This project has 36 loans. Any sentence like "the most-read subject is Fiction" has to come with its "with 12 of 36 loans" alongside. And there are two results the report must present with their warning: the "third heaviest reader" at the Biblioteca Infantil has zero loans because that branch only has three members (QR-13), and the average days per subject is computed over 6 or 8 loans, not over thousands. Writing that down doesn't make you look unsure: it makes you look like someone to trust.
And the control figure, always visible. Every table in the report should be reconcilable with a known total. This project's: 36 loans, 20 copies, 12 works, €27.20 in fines. If a breakdown doesn't add up to that, the breakdown is wrong — and if the reader can check it at a glance, they'll trust the rest.
- The defence: the questions you'll be asked
It makes no difference whether it's an examining board, a review with your team or a job interview with the project on the table: the questions are almost always the same six. Prepare them in writing, in two or three sentences each.
| Question | What they're checking | How to answer it well |
|---|---|---|
| "Why two tables for the book?" | Whether you understand the relational model or have copied it | With the concrete example: three copies across two branches; the work is catalogued and reserved, the copy is lent; without the separation, you can't know which one came back |
| "How do you stop two people lending the same copy at once?" | Whether you know the application can't fix that | The partial unique index WHERE return_date IS NULL, and why a normal UNIQUE doesn't work (the nulls) and a CHECK can't (it doesn't see other rows). And the limit: it doesn't cover historical overlaps, that's what EXCLUDE is for |
| "What if it grows ten times over?" | Whether you've thought about volume | Which query breaks first and why; which index holds it up; and what you'd stop computing on the fly — the monthly series would become a materialized view (10-01) |
"Why do you store due_date if it can be computed?" |
Whether you tell a fact from a consequence | It's a historical fact that also moves with renewals. The same argument as unit_price in GreenStore |
| "What would you do differently?" | Self-criticism | Two or three concrete things from section 7 of the report. Never "nothing": it's the worst possible answer |
| "Show me the query that gave you most trouble" | Whether the project is yours | Open it, explain the FROM first and then the key decision. If you can't explain it without reading it, it wasn't yours |
Three preparation tips worth more than rehearsing a speech:
- Have the
EXPLAINto hand, in a file, with the before and the after. It's the answer to half a dozen questions and it can't be improvised. - Have the database loaded and
psqlopen. If somebody asks "and how many members have debt?", answering by running it is worth ten times answering from memory. - Prepare the two-minute summary. Domain, central twist (work/copy), what gave you most trouble and one known limitation. It's what you'll say in the lift, in the interview and in the first sentence of the defence.
- Publishing the project
A project that can't be run doesn't exist. The bar is concrete: somebody who has never seen it must have it working in five minutes.
alvorada-library/
├── README.md ← the first and most important thing
├── sql/
│ ├── 01-schema.sql
│ ├── 02-data.sql
│ ├── 03-queries.sql
│ └── 99-checks.sql ← counts, INSERTs that must fail, privileges
├── report/
│ ├── 04-report.md
│ └── er-model.png
└── migrations/ ← optional, but it adds marks (05-06)
├── V1__initial_schema.sql
└── V2__title_index.sqlThe five-minute README
Five sections and not one more: what it is (two lines), requirements (PostgreSQL 16), how to run it (the commands, copyable), what to expect (the table of counts, so they know whether it went well) and where the report is.
createdb library
psql -d library -f sql/01-schema.sql
psql -d library -f sql/02-data.sql
psql -d library -f sql/99-checks.sql # must give 3 · 1 · 3 · 6 · 3 · 1 · 1 · 2That last line is the detail that separates a good README from a correct one: it tells the reader how to know it worked. Without it, they run the three files, see no errors and are left not knowing whether the load is complete.
Migrations, and why they matter even when nobody asks for them
01-schema.sql is idempotent because it drops and recreates: perfect for a deliverable, impossible in production, where dropping the tables means dropping the data. A section of the report saying "for production, this would become versioned migrations with Flyway or Liquibase, and the title index change would be a CREATE INDEX CONCURRENTLY so as not to lock the table" shows you know the difference (05-06, 11-05). Adding two example migration files takes ten minutes and it shows.
What never gets uploaded
- Credentials. Not in the
README, not in a.env, not in a comment, not "temporarily". Git remembers: a password pushed and deleted afterwards is still in the history. Use a.env.examplewith fake values and a.gitignorethat excludes the real.env. - Real personal data. Not customers', not colleagues', not your own. The project's are made up and the emails go to
@example.com, a domain reserved for exactly this (SR-07). - Production dumps. A
pg_dumpof a real database in a repository is a data breach, not a test file. - 200 MB files. If you need volume, upload the generator (
generate_series), not the generated data.
- Self-assessment: the rubric as a checklist
12-02's rubric, turned into yes-or-no questions. Answer them before handing in, with the project in front of you and being hard on yourself.
Model (25 %)
- [ ] Are
worksandcopiestwo tables, and doesloanshang offcopies? - [ ] Does
reservationshang offworks, notcopies? - [ ] Does the N:M
works_authorshave a composite PK and data of its own (role,credit_order)? - [ ] Does
librarianshave its self-referencing FK, nullable only at the top? - [ ] Is every denormalisation written down and justified in the report?
Integrity (20 %)
- [ ] Are the 14 IRs declared, named and annotated with their number?
- [ ] Is IR-03 solved in the database with the partial unique index?
- [ ] Have you run the
INSERTs that must fail, and do they fail? - [ ] Is every
ON DELETEconsistent with the semantics of its relationship?
Queries (25 %)
- [ ] Do all fifteen run without a single error?
- [ ] Does none of them lose rows through a badly chosen
JOIN, or duplicate them? - [ ] Do all the rankings have an explicit tie-breaker?
- [ ] Does every metric carry its definition in a comment?
- [ ] Have you validated at least two totals by two routes?
Performance (10 %) · Security (10 %) · Delivery (10 %)
- [ ] Does every index have a concrete query that uses it, and have you written down what you decide not to index?
- [ ] Are there two
EXPLAINs before/after with enough volume for them to mean something? - [ ] Do the three roles exist, does none of them have
DELETEon the history, and do you prove it withhas_table_privilege? - [ ] Is all the personal data fictitious and are the account closures logical?
- [ ] Do the four files run in order on an empty database, twice in a row?
- [ ] Does the report have the seven sections, including the one on limitations?
If you answer no to any of them, fix it before handing in. None of these boxes takes more than half an hour, and each one is worth more than rewriting a query that already worked.
Common Mistakes and Tips
- Writing the report on the last night. Decisions are justified far better on the day they're taken. A
decisions.mdfile open from day one turns itself into section 3. - Telling the process instead of the result. Nobody cares that you first had a
bookstable and then split it. What matters is the final model and why. The process only appears if it explains a decision. - Showing the fifteen queries in the report. They're in
03-queries.sql. In the report there go three or four, chosen, with their result and their reading. - Publishing a percentage over 6 cases. At these volumes, almost every percentage lies. Absolute figure and denominator.
- Saying "nothing" when they ask what you'd do differently. It's the answer that goes down worst: it means you haven't looked at your own work from a distance. Have two ready.
- A
READMEthat starts with the architecture. Start with how to run it. The architecture is read by somebody who already has it working. - Tip: ask somebody to run it in front of you without helping them. In ten minutes you'll know what's missing from the
README, and there's no cheaper way to find out. - Tip: read the report out loud. The sentences you can't read without stumbling are the ones nobody is going to understand.
- Tip: keep a copy of the project exactly as you handed it in. A year from now you'll want to show it, and you'll want it to be the one you defended, not one you touched afterwards.
Exercises
Exercise 1
Write section 3 of the report (design decisions) in full: between six and eight decisions, each with its discarded alternative, the reason and the price you pay. It must include the work/copy separation, due_date, the partial index and the reservation queue.
Exercise 2
Prepare the two-minute defence: write it, time it and cut it until it fits. It must contain the domain, the model's central twist, the technical decision you're proudest of, a concrete figure and a known limitation.
Exercise 3
A colleague writes in their report: "The Biblioteca Central is clearly the most efficient in the network: it accounts for 55.6 % of loans." (1) Is the figure true? (2) Give three reasons why the conclusion doesn't hold. (3) What would you write instead?
Solutions
Solution 1 — There's no single answer, but there is a template that marks well, four elements per decision. Here's an example, so you can see the level of detail expected:
copiesas a table of its own. Discarded alternative: anum_copiescolumn inworks, like thestockof a commercial catalogue. Why: the copy has its own branch, its own physical status and its own history, and without a row of its own it would be impossible to know which of the three came back; the loan would no longer be able to point at a specific object. What it costs me: one more table, one moreJOINin almost every catalogue query, and "how many books are there" now having two legitimate answers —12 works and 20 copies— that have to be distinguished in every report.
And the signs that the section is badly written: decisions with no alternative ("I used surrogate keys"), straw-man alternatives (comparing your solution with an absurd one), and no mention of the price. Every design decision has a cost; if you can't find it, you haven't decided anything.
Solution 2 — The structure that fits in two minutes, with the timings:
| Time | Content |
|---|---|
| 0:00-0:20 | The domain: a network of three public libraries replacing its spreadsheets. Catalogue, loans, reservations and fines |
| 0:20-0:50 | The twist: work versus copy. "There are three copies of The Garden of Hours across two branches: the work is catalogued and reserved, the copy is lent" |
| 0:50-1:20 | The technical decision: the partial unique index that guarantees a single active loan per copy, declared in the database and not in the application |
| 1:20-1:40 | A figure: 36 loans, 6 active, 3 overdue, €27.20 in fines of which €13.40 is still to be collected |
| 1:40-2:00 | A limitation: the member's suspension isn't recomputed on its own; today it depends on an external process |
The typical mistake is spending 90 seconds on the list of tables. Nobody remembers a list of tables; everybody remembers "three copies across two branches".
Solution 3 — (1) The figure is true: 20 of 36 loans are the Central's, which is 55.6 %. The arithmetic is fine. (2) The conclusion doesn't hold, for three reasons. (a) "Efficiency" isn't "volume". The Central has 10 of the 20 copies and 7 of the 15 members; with half the holdings, doing 56 % of the loans is almost exactly what you'd expect. An efficiency metric would be loans per copy —2.0 at the Central against 2.0 at the Infantil, which with 3 copies did 6 loans—, and then the conclusion disappears. (b) The branches aren't comparable: the Infantil opened in 2024 and the other two in 2018 and 2021, and their public and their holdings are of a different nature. (c) The sample size: 36 loans in total and 6 at the Infantil; a single loan moves several percentage points.
(3) Something like this: "The Central accounts for 20 of the 36 loans (55.6 %), in line with its share of the holdings: it looks after 10 of the 20 copies. Normalised per copy, the three branches perform similarly (2.0 · 1.4 · 2.0 loans per copy), over volumes too small to claim any difference." Less emphatic and far more useful — which is, almost always, the difference between an analysis and a headline.
Course Conclusion
This is where it ends, so it's worth looking back before closing.
You started not knowing what a table was and you've just designed an entire system. That's the journey:
flowchart LR
A["<b>M1-M2</b><br/>Fundamentals<br/>and SELECT"] --> B["<b>M3-M4</b><br/>JOIN, filtering<br/>and aggregation"]
B --> C["<b>M5-M6</b><br/>Writing data<br/>and functions"]
C --> D["<b>M7-M8</b><br/>Subqueries<br/>and indexes"]
D --> E["<b>M9-M10</b><br/>Transactions<br/>and advanced SQL"]
E --> F["<b>M11</b><br/>The craft"]
F --> G["<b>M12</b><br/>Final<br/>project"]
- Modules 1 and 2 — the foundations. What SQL is and where it fits, PostgreSQL installed, the data types, the relational model with its keys and its normalisation, and GreenStore loaded. Then
SELECT: columns, aliases,WHERE,DISTINCT,ORDER BYand pagination withLIMITand with keyset. - Modules 3 and 4 — combining and summarising. The
JOINs in all their forms, theLEFT JOINand the anti-join that find what isn't there, the self join over self-referencing relationships and the set operators. And fine-grained filtering —LIKE,IN,BETWEEN, the three-valued logic ofNULLs— with aggregation,GROUP BYandHAVING. - Modules 5 and 6 — writing and transforming.
CREATE TABLEwith its six constraints,INSERT,UPDATE,DELETEand the soft delete, the upsert and migrations withALTER TABLE. And the functions: strings, numbers, dates,CAST,COALESCE,CASEand the pivot. - Modules 7 and 8 — depth and speed. Scalar, correlated,
EXISTS, derived andLATERALsubqueries, with the criteria for choosing between aJOIN, a subquery and a CTE. And the indexes: how a B-tree works, which ones to create and which not, andEXPLAINso you can stop guessing. - Modules 9 and 10 — reliability and power. Transactions, ACID, MVCC,
SAVEPOINT, isolation levels, locks andSKIP LOCKED. And the big tools: views and materialized views, CTEs and recursive ones, window functions, procedures, triggers and JSON. - Module 11 — the craft. The use cases that come back again and again, the best practices and their antipatterns, security with SQL injection and the role model, data analysis where defining matters more than querying, and the real SQL of a web application with its pool, its ORM and its N+1.
- Module 12 — doing it yourself. A new domain, a brief with all the imprecision of a real brief, and out of it a complete system: twelve tables, fourteen declared constraints, fifteen queries, seven justified indexes, three roles and a report.
What you can do now that you couldn't at the start. It isn't "writing queries": that's the easy part. It's this:
- Reading a business brief and getting a model out of it, telling entities from attributes and spotting when the same word means two things.
- Making the database defend its own rules, instead of trusting everybody to remember to validate.
- Writing a query and knowing whether it's right, by checking row counts and validating totals by two routes.
- Knowing why a query is slow and which index fixes it, with an execution plan in front of you instead of a hunch.
- Deciding what gets stored and what gets computed, what gets encapsulated and what doesn't, and defending every decision with its alternative and its price.
- Presenting a result without misleading in good faith: with its definition, its denominator and its sample size.
Where to go next. None of these paths is compulsory, and they all start where this course ends:
| Path | What you'll find | When it makes sense |
|---|---|---|
| PostgreSQL administration | Backups and PITR, replication, autovacuum, memory tuning, partitioning, extensions |
If the system is yours and it has to be standing at three in the morning |
| Advanced modelling | Temporal keys and historisation, inheritance patterns, range types, evolving schemas | If you design systems for a living and don't just query them |
| Data warehousing and dimensional modelling | Facts and dimensions, star schema, SCD, ELT | If the work is analytical and reports have to be served to a lot of people |
| Analytical and columnar engines | ClickHouse, DuckDB, BigQuery, Snowflake, Parquet | When queries read millions of rows and write few |
| NoSQL, and when it makes sense | Document, key-value, graph, time series | When the relational model genuinely gets in the way: and with what you know now, you'll be able to judge that instead of following fashion |
And the reading, short and well chosen: the official PostgreSQL documentation, which is among the best there is and reads surprisingly well; SQL Performance Explained, by Markus Winand, for indexes and plans; Designing Data-Intensive Applications, by Martin Kleppmann, to understand what's underneath all this; and The Art of PostgreSQL, by Dimitri Fontaine, to keep writing better SQL.
One last thing remains, and it's the one that really makes the difference: using it. SQL that's learned and not practised is forgotten in six months, and SQL that's used sharpens itself. Take the project you've just finished and stretch it —add interlibrary loans, an audit trail, notifications—; offer to write the report somebody on your team pulls together by hand every month; open the console of the database you already use and ask it something nobody has ever asked it.
Twelve modules on, the sentence the course started with comes back with a different meaning: SQL is the language you use to talk to data. You speak it now. What you say with it is up to you.
SQL Course
Module 1: Introduction to SQL
- What is SQL?
- Setting up your SQL environment
- Basic SQL syntax
- Understanding databases and tables
- The relational model: primary and foreign keys
- The course database: GreenStore
Module 2: Basic SQL queries
- The SELECT statement
- Aliases, expressions and calculated columns
- Filtering data with WHERE
- DISTINCT and removing duplicates
- Sorting data with ORDER BY
- Limiting results with LIMIT
Module 3: Working with multiple tables
- JOIN operations
- INNER JOIN
- LEFT JOIN
- RIGHT JOIN
- FULL OUTER JOIN
- SELF JOIN and CROSS JOIN
- Set operations: UNION, INTERSECT and EXCEPT
Module 4: Advanced data filtering
- Using LIKE for pattern matching
- The IN and BETWEEN operators
- NULL values and IS NULL
- Aggregate functions: COUNT, SUM, AVG, MIN and MAX
- Aggregating data with GROUP BY
- The HAVING clause
Module 5: Data manipulation
- Creating tables and constraints with CREATE TABLE
- The INSERT statement
- The UPDATE statement
- The DELETE statement
- The UPSERT (MERGE) statement
- Changing the schema: ALTER TABLE and safe migrations
Module 6: Advanced SQL functions
- String functions
- Numeric functions
- Date and time functions
- Type conversion and handling NULL: CAST and COALESCE
- Conditional expressions
Module 7: Subqueries and nested queries
- Introduction to subqueries
- Correlated subqueries
- EXISTS and NOT EXISTS
- Using subqueries in SELECT, FROM and WHERE
- Subquery or JOIN: which one to choose
Module 8: Indexes and performance tuning
- Understanding indexes
- Creating and managing indexes
- Index types and when not to index
- Query optimization techniques
- Analyzing query performance
Module 9: Transactions and concurrency
- Introduction to transactions
- ACID properties
- Transaction control statements
- Isolation levels and concurrency anomalies
- Handling concurrency: locks and deadlocks
Module 10: Advanced topics
- Views
- Common table expressions (CTEs)
- Window functions
- Stored procedures
- Triggers
- JSON and semi-structured data
Module 11: SQL in practice
- Real-world use cases
- Best practices
- Security: SQL injection, permissions and roles
- SQL for data analysis
- SQL in web development
