Helena's email is the context; this is the contract. A requirement is only useful if it can be checked: "the system must handle loans properly" isn't a requirement, it's a wish. "A copy can't appear in two loans with return_date IS NULL, and the database must reject the attempt" is one, because there's an objective way to find out whether it holds: try it and see whether it fails.
This lesson is that specification, grouped into six blocks —data, integrity, query, performance, security and delivery— and closed with the rubric the project is marked against, so you can assess yourself before calling it finished. Within each block there are two layers: what isn't negotiable, because it's what the project sets out to teach, and what's left to your judgement, because designing means choosing and in 12-04 you'll see that several different solutions are equally correct.
Contents
- Mandatory conventions
- Data requirements (DR)
- Integrity requirements (IR)
- Query requirements (QR): the 15 queries
- Performance requirements (PR)
- Security requirements (SR)
- Delivery requirements (DL)
- Quality criteria
- Marking rubric
- Common Mistakes and Tips
- Exercises
- Conclusion
- Mandatory conventions
They're the course's own, and they're taken as read (05-01, 11-02). They're listed because they form part of the marking:
| Element | Convention |
|---|---|
| Identifiers | snake_case, singular for columns and plural for tables, plain ASCII with no accented characters |
| Primary key | id INTEGER GENERATED BY DEFAULT AS IDENTITY (or composite in pure bridge tables) |
| Foreign key | <referenced_table>_id, always with an explicit ON DELETE |
| Constraints | Named: pk_, fk_, uq_, chk_; indexes idx_ |
| Money | NUMERIC(10,2). Never floating point |
| Dates | DATE, except where the instant is needed |
| Nullability | NOT NULL by default; every NULL allowed is a documented decision |
| Query style | Explicit AS on column aliases, table aliases by initial, one clause per line |
| Reference date | DATE '2026-06-30' instead of CURRENT_DATE, so the results are reproducible |
- Data requirements (DR)
These are the minimum mandatory entities. You can add columns, and you can add tables if you justify them; what you can't do is remove any of these or merge two of them.
| # | Entity | Essential attributes | Notes |
|---|---|---|---|
| DR-01 | branches |
name (unique), address, opening date | 3 rows minimum |
| DR-02 | subjects |
name (unique) | Equivalent to categories |
| DR-03 | publishers |
name (unique), country | Equivalent to suppliers |
| DR-04 | authors |
first name, last name, nationality, birth year | 8 rows minimum |
| DR-05 | works |
title, subject, publisher, publication year, ISBN | No stock column |
| DR-06 | works_authors |
work, author, role, credit order | N:M mandatory, with a composite PK |
| DR-07 | copies |
work, branch, barcode (unique), acquisition date, physical status | One row per physical object |
| DR-08 | members |
first name, last name, id card (unique), date of birth, type, status, signup branch, signup date | type and status with a closed domain |
| DR-09 | librarians |
first name, last name, job title, branch, supervisor, email (unique) | Self-referencing hierarchy mandatory |
| DR-10 | loans |
copy, member, librarian, loan date, due date, actual return date, renewals | One row per copy lent |
| DR-11 | reservations |
work, member, pickup branch, reservation date, status, notification and closing dates | Hangs off works |
| DR-12 | fines |
loan (unique), amount, days late, issue date, payment date | Its own table, not a column |
What isn't negotiable, and why:
- The work / copy separation (DR-05 + DR-07). It's the point of the project. A single
bookstable with a counter is an automatic fail, however well written the rest may be. - The N:M
works_authors(DR-06). Anauthorcolumn inworksdoesn't allow two authors, and anauthor1/author2pair is the repeated-columns antipattern (01-05). Here, yes: a composite PK(work_id, author_id)with noidof its own — it's a pure bridge table and that was the alternative 05-01 described. - The librarian hierarchy (DR-09).
supervisor_idreferencing its own table, nullable only at the top. It's what feeds 03-06'sSELF JOINand 10-02's recursive CTE. - The history that is never deleted (DR-10). No
DELETEonloans. Closing member and copy records is logical, and the FKs pointing atloansmust physically prevent the parent from being deleted. loanshangs offcopiesandreservationshangs offworks. Inverting either one breaks the model.
What's left to your judgement: whether publishers is a table or a text column (a table is asked for, but it's open to discussion), whether works carries a language or a page count, whether members stores a phone number, whether copies stores the shelfmark, whether reservations has a status table or a CHECK, and how you distinguish a lost copy from a withdrawn one.
Minimum volume of test data: 3 branches, 6 subjects, 5 publishers, 8 authors, 10 works, 15 copies, 12 members, 25 loans, 4 reservations and 4 fines. Any less and you can't check the queries; a great deal more adds nothing when it's done by hand. (12-03's reference data set uses 3 / 6 / 5 / 10 / 12 / 20 / 15 / 36 / 5 / 6.)
- Integrity requirements (IR)
All these rules must be declared in the database. The application checking them as well is fine; only the application checking them is exactly what 05-01 called "trusting everybody to remember to validate".
| # | Rule | How it's declared |
|---|---|---|
| IR-01 | The due date is later than the loan date | CHECK (due_date > loan_date) |
| IR-02 | The return date isn't earlier than the loan date | CHECK (return_date IS NULL OR return_date >= loan_date) |
| IR-03 | A copy can't be on two active loans | The hard one. See below |
| IR-04 | Renewals are between 0 and the maximum (BR-03) | CHECK on the range; the per-type cap goes in the logic |
| IR-05 | A fine's amount is never negative, and its days late are > 0 | Two CHECKs |
| IR-06 | A fine can't be paid before it's issued | CHECK (paid_date IS NULL OR paid_date >= issued_date) |
| IR-07 | A loan has at most one fine | UNIQUE (loan_id) on fines |
| IR-08 | The ISBN, if present, is unique | UNIQUE (isbn), nullable |
| IR-09 | The copy's barcode is unique and mandatory | NOT NULL UNIQUE |
| IR-10 | The member's type and status belong to their domain | CHECK (... IN (...)) |
| IR-11 | The copy's status and the reservation's status, likewise | Two CHECKs |
| IR-12 | A librarian can't be their own supervisor | CHECK (supervisor_id <> id) |
| IR-13 | A member can't have two live reservations for the same work | A partial unique index over the live statuses |
| IR-14 | You can't delete a member, a copy or a work that has history | ON DELETE RESTRICT on the FKs of loans |
The hard one: IR-03
This requirement deserves its own section because it's where you find out whether you've understood module 8. The rule is: there can be many loans of copy 1 —its entire history— but at most one with return_date IS NULL.
The four possible ways out, and why three of them don't work:
| Attempt | Problem |
|---|---|
UNIQUE (copy_id) |
Forbids the history: copy 1 couldn't be lent twice in its life |
UNIQUE (copy_id, return_date) |
Doesn't work: NULL isn't equal to NULL (04-03, 05-01), so it allows infinitely many active loans |
A CHECK with a subquery |
Illegal: a CHECK can't query other rows or other tables (05-01) |
An active BOOLEAN column with a partial UNIQUE |
It works, but it duplicates information: active and return_date IS NULL would say the same thing and could contradict each other |
The solution the project asks for is a partial unique index (08-02): an index that contains only the rows meeting a condition, and which therefore imposes uniqueness only among them.
It reads literally: "among loans not yet returned, copy_id is unique". It's declarative, the engine enforces it, it takes up only as many entries as there are live loans —six, not hundreds of thousands— and as a bonus it speeds up every active-loan query. In 12-03 it's implemented and in 12-04 it's compared with the EXCLUDE constraint alternative.
Dialect note: partial indexes exist in PostgreSQL, SQLite and (with caveats) SQL Server, where they're called filtered indexes. MySQL doesn't have them, and there the usual solution is a generated column that is
NULLwhen the loan is closed, plus aUNIQUEon it — becauseUNIQUEignores nulls.
- Query requirements (QR): the 15 queries
These fifteen queries are the 03-queries.sql deliverable. They're ordered by increasing difficulty and each one carries the technique it exercises and the lesson where it was taught, so you know where to go back to if you get stuck. They all use DATE '2026-06-30' as the reference date.
| # | Query | Technique | Lesson |
|---|---|---|---|
| QR-01 | Works published since 2015, with their ISBN, ordered by year descending and title | WHERE, ORDER BY with a tie-breaker |
02-04, 02-06 |
| QR-02 | Copies at a given branch, with the title of their work and its subject | Four-table JOIN |
03-02 |
| QR-03 | Active loans: member, work, branch, days out and days late | JOIN + IS NULL + date arithmetic |
03-02, 04-03, 06-03 |
| QR-04 | The system's three gaps: members with no loans, works with no copies and copies never lent, in a single result | Anti-join + UNION ALL |
03-03, 03-07, 07-03 |
| QR-05 | Subjects with 5 or more loans, with their average duration | GROUP BY + HAVING |
04-05, 04-06 |
| QR-06 | Availability by work: total copies, enabled, currently on loan and available | Conditional aggregate with FILTER |
04-04, 06-05 |
| QR-07 | Works signed by more than one author, with their authors in a single cell and in credit order | N:M + string_agg |
03-02, 04-04 |
| QR-08 | Fines by member type: total, collected and outstanding, with the late rate | FILTER + COALESCE + a defined metric |
06-04, 11-04 |
| QR-09 | Members with more loans than the average for their type | Correlated subquery | 07-02, 07-03 |
| QR-10 | The waiting reservation queue, with each member's position in their work's queue | ROW_NUMBER() with a partition |
10-03 |
| QR-11 | The 3 most-borrowed works at each branch | LATERAL (or a filtered window) |
07-04 |
| QR-12 | Loans per month for the last 12 months, with no gaps, with a running total and a 3-month moving average | generate_series + LEFT JOIN + windows |
11-01, 10-03 |
| QR-13 | The 3 heaviest readers at each branch, with an explicit tie-breaker | RANK / ROW_NUMBER over a partition |
10-03 |
| QR-14 | Librarian org chart with each person's level and hierarchical path | Recursive CTE | 10-02 |
| QR-15 | Pivoted report: loans by branch (rows) and subject (columns), without losing a single branch | Pivot with FILTER + LEFT JOIN |
06-05, 11-04 |
Five conditions that apply to all fifteen:
- Every query is preceded by a comment saying what question it answers and, if it handles a metric, how it defines it (11-04). Does "loans" include the active ones? What about those of closed members? Write it down.
- No query may lose rows through a badly chosen
JOIN. QR-06, QR-12 and QR-15 have to show the zeros: the work with no copies, the month with no loans and the branch with nothing in a subject. - Every ranking's
ORDER BYneeds a tie-breaker. Without one, two runs can return different orders and the report stops being reproducible. - Every query delivered comes with its result. The SQL isn't enough: you have to have run it.
- No query uses
SELECT *. Explicit columns, always (11-02).
- Performance requirements (PR)
The project is tested with dozens of rows, but it's designed for tens of thousands of members and hundreds of thousands of loans. The indexes are justified at that size, not at the size of the test file.
| # | Requirement |
|---|---|
| PR-01 | An index on every foreign key used for navigation: PostgreSQL indexes the PK, not the FK (08-01). loans(copy_id), loans(member_id), copies(work_id), copies(branch_id), works(subject_id) |
| PR-02 | The partial unique index from IR-03, which also resolves the active-loans query |
| PR-03 | An index that serves the overdue query and the time series: on loans(loan_date) and on the columns the overdue check filters by |
| PR-04 | An index for title search, with a reasoned choice between a B-tree over an expression, pg_trgm or full-text search (08-03) |
| PR-05 | Every index is justified with the specific query that takes advantage of it. An index with no query to use it is an index that only slows writes down (08-02) |
| PR-06 | The report includes the EXPLAIN of at least two queries before and after creating their index, with the change of plan commented (08-05) |
| PR-07 | You state what you've decided not to index, and why |
PR-05 and PR-07 are the ones that actually get marked. Adding ten indexes is easy; explaining why those ten and not others is what shows judgement.
- Security requirements (SR)
Three roles, following 11-03's model: privileges are granted to group roles and users are made members of them, never the other way round.
| # | Role | Permissions |
|---|---|---|
| SR-01 | lib_read |
SELECT on the catalogue (works, copies, authors, subjects, publishers, branches) and on the public views. No access to members, loans or fines |
| SR-02 | lib_desk |
Everything above, plus SELECT/INSERT/UPDATE on loans, reservations and fines, and SELECT/UPDATE on members. No DELETE on any of them (DR-10) |
| SR-03 | lib_admin |
Everything above, plus DDL and catalogue management. It isn't the owner and isn't a superuser |
And four requirements about personal data, which here aren't decoration: a loan history is a record of what a person reads, one of the most sensitive pieces of data a library can hold.
- SR-04. No application role has
DELETEon the history, and none owns the tables. - SR-05. Management reports are served from aggregate views that don't expose what each member has read. Whoever needs the aggregate figure doesn't need the individual one.
- SR-06. Closing a member's account is logical; the subsequent anonymisation (replacing name, id card and email with neutral values while keeping the
id) must be planned for, so an erasure request can be met without destroying the statistics. - SR-07. The test data is entirely fictitious: made-up names, id numbers with a valid format but no real holder, and emails at
@example.com, a domain reserved for precisely this. Never use real data, not even your own, in a file that's going to end up in a repository.
Optionally, and as an exercise from 11-03: implement RLS so a librarian only sees the members of their own branch. It adds to the rubric, but it isn't mandatory and it isn't worth delivering it badly.
- Delivery requirements (DL)
| File | Contents | Must satisfy |
|---|---|---|
01-schema.sql |
DROP in reverse order, CREATE TABLE in dependency order, named constraints, indexes, views |
Idempotent: running it twice leaves the same state |
02-data.sql |
Test data, in dependency order, with a final setval if you insert explicit ids |
Runnable after the schema, without errors |
03-queries.sql |
The 15 queries, numbered and commented | Read-only: not one INSERT, not one UPDATE |
04-report.md |
The report, following 12-05 | Between 4 and 8 pages |
DL-01. The files are run in order: 01, 02, 03. The whole sequence must work in one go on an empty database:
createdb library
psql -d library -f 01-schema.sql
psql -d library -f 02-data.sql
psql -d library -f 03-queries.sqlDL-02. The schema is idempotent by the same mechanism as greenstore.sql (01-06): DROP TABLE IF EXISTS ... CASCADE at the top, in reverse dependency order. That's the right thing for a create-from-scratch script; for a production system it would be versioned migrations (05-06), and saying so in the report earns marks.
DL-03. The files go into a git repository with their README.md (12-05), with no credentials of any kind.
DL-04. Every query in 03-queries.sql carries its QR-nn number above it, the question it answers and the lessons it applies.
- Quality criteria
These aren't numbered requirements: they're the difference between a project that works and one that is also well made. They come straight out of 11-02.
- Uniform formatting. Keywords in upper case, one clause per line, short and consistent table aliases, stable indentation. If two queries in the same file look different, it shows.
- Names that don't need a comment.
return_dateyes;rdno.uq_active_loan_per_copyyes;idx3no. - Comments that explain the why, not the what.
-- The LEFT JOIN is mandatory: there are works with no copies at allis useful.-- Joins works with copiesis noise. - Not one query left unrun. An
03-queries.sqlwith a query that errors is the most expensive failure of all, because it costs nothing to avoid. - Not one implicit definition. Every metric in the report says what it includes and what it excludes (11-04).
- No over-engineering. Three triggers, five materialized views and an audit table in a twelve-table project don't add up: they subtract, because they have to be maintained and defended.
- Marking rubric
Use it as a self-assessment checklist before you hand in. The weight column shows how much each block counts.
| Criterion | Weight | What's looked at |
|---|---|---|
| Data model | 25 % | Work/copy separation; correct N:M; self-referencing hierarchy; normalisation without excess; justified denormalisation decisions |
| Integrity | 20 % | The 14 IRs declared and named; IR-03 solved with a partial index; ON DELETE consistent with the semantics of each relationship |
| Queries | 25 % | All 15 run and are correct; no lost or duplicated rows; sorts with tie-breakers; defined metrics |
| Performance | 10 % | Indexes justified one by one with their query; two commented EXPLAINs; what you decide not to index |
| Security | 10 % | Three least-privilege roles; no DELETE on the history; fictitious personal data and soft account closure |
| Delivery and report | 10 % | The four files run in order; uniform style; report with model, decisions, results and limitations |
Four failures wipe out a whole block, however good the rest may be:
- A single
bookstable instead ofworks+copies→ model scores zero. - IR-03 unsolved or solved only in the application → integrity scores zero.
- A query in the deliverable that errors when run → queries score zero.
- Real personal data in the repository → delivery scores zero.
Common Mistakes and Tips
- Reading the requirements once, at the start. Come back to this lesson at the end of each block and cross off what's done. Half of what gets lost on the rubric is forgotten material, not badly done material.
- Treating the minimums as targets. "12 members" is the floor for the queries to mean anything, not the goal. But more than a couple of hundred rows by hand is wasted time: for volume,
generate_series(12-03). - Confusing requirement with solution. IR-03 says what must hold; the partial index is how we solve it here. If you find another way that also guarantees it in the database, it's valid — and 12-04 has one.
- Solving in the application what the database is asked for. "My code already checks it before inserting" doesn't satisfy IR-03. Two simultaneous requests slip straight through, and that's exactly what module 9 was explaining.
- Writing the fifteen queries in one sitting and running them at the end. Run each one as soon as you write it and check the row count at every
JOIN(11-04). A mistake in the third contaminates the twelve that follow. - Tip: turn this lesson into a
requirements.mdfile in the repository, with checkboxes. It's 11-02's checklist applied to your own project, and it's what whoever marks you will look at. - Tip: write the hardest query you can see first (probably QR-11 or QR-12). If the model stands up to the hardest one, it stands up to the other fourteen; if it doesn't, better to find out before loading the data.
- Tip: save the output of every query to a file. When you change the schema or the data, you'll be able to compare and see what moved. It's 11-04's "control figure" applied to the project.
Exercises
As in 12-01, these are project tasks: they come with a sketch or a rubric, not a full solution.
Task 1 — The plan of attack
Turn the requirements into a work plan of your own: a table with the tasks, their dependencies on each other, an estimate in hours and the requirement each one closes. It must cover everything from the model to the delivery. If your plan doesn't include an explicit "generate test data with edge cases" task, it's incomplete.
Task 2 — Predicting the hard part
Before writing any SQL, reason about IR-03: (1) why doesn't UNIQUE (copy_id, return_date) work in PostgreSQL, and in which engine would it work? (2) Write the exact INSERT that should fail and the one that must keep working. (3) What happens to the partial index if tomorrow it's decided that a copy can be lent "for reading room use" at the same time as it's out on a home loan?
Task 3 — Defining the metrics
Before QR-08, write the exact definition of these five metrics, saying what's in and what's out, in the style of 11-04's definitions table: loans in the period, active member, late rate, outstanding debt and available work. For each one, also say what other reasonable definition exists and how it would change the figure.
Solutions
Rubric for Task 1
An acceptable plan has between 8 and 12 tasks and respects these dependencies: model → DDL → data → queries → indexes → views and security → report. The two planning mistakes that come up again and again are leaving the test data until the end —and only then discovering that the model can't represent a case— and leaving the report until the last day, when you no longer remember why you made half your decisions. Write the report as you decide.
Solution to Task 2
(1) Because in the SQL standard, and in PostgreSQL, NULL isn't equal to NULL, so two rows with (1, NULL) are not considered duplicates and the UNIQUE accepts both (04-03, 05-01). In SQL Server it would fail, because it treats all NULLs as equal for the purposes of a unique index — and in PostgreSQL 15+ you can imitate that with UNIQUE NULLS NOT DISTINCT, although for this case the partial index is still better because it also indexes only the live rows.
(2) A second open loan of the same copy must fail, and another closed loan of the same copy must keep working:
-- ⚠️ INCORRECT: copy 1 already has a loan that hasn't been returned
INSERT INTO loans (copy_id, member_id, librarian_id, loan_date, due_date)
VALUES (1, 13, 5, DATE '2026-06-25', DATE '2026-07-16');
-- ✅ CORRECT: this is history, not a live loan
INSERT INTO loans (copy_id, member_id, librarian_id, loan_date, due_date, return_date)
VALUES (1, 13, 5, DATE '2024-01-10', DATE '2024-01-31', DATE '2024-01-28');The first one returns:
ERROR: duplicate key value violates unique constraint "uq_active_loan_per_copy" DETAIL: Key (copy_id)=(1) already exists.
(3) The index would stop being valid as it stands, because it would no longer be "at most one active loan" but "at most one of each kind". The solution would be to add a loan_type column and include it in the index: ON loans (copy_id, loan_type) WHERE return_date IS NULL. It's a good reminder that a constraint encodes a specific business rule, and that when the rule changes, the constraint changes with it — which, by the way, is an advantage: if the rule lived scattered through the application code, nobody would know where to touch it.
Sketch for Task 3
Two of the five, to set the level of detail expected:
| Metric | Project definition | Reasonable alternative |
|---|---|---|
| Loans in the period | Rows of loans with a loan_date inside the period, every state, including those still open and those of members whose accounts were later closed |
Counting only the closed ones, so you can talk about average duration. It gives a smaller figure and is no good for measuring demand |
| Outstanding debt | SUM(amount) of fines with paid_date IS NULL. It doesn't include overdue loans not yet returned, which haven't generated a fine yet (BR-10) |
Including the potential debt of overdue loans, computed as of today. That's the figure management cares about, and it has to be called something else so it doesn't get mixed up with the accounting one |
The underlying lesson is 11-04's: there's no correct definition, there's a written definition. The serious problem isn't choosing badly; it's publishing two different figures in the same report without saying how they differ.
Conclusion
You now have the project's contract:
- 12 data requirements with the minimum entities and their attributes. The non-negotiable parts: the work / copy separation, the N:M
works_authorswith a composite PK, the self-referencing hierarchy of librarians, the history that is never deleted, andloanshanging offcopieswhilereservationshangs offworks. - 14 integrity requirements, all declared in the database and all named. The hard one is IR-03 —one copy, a single active loan—, which can't be solved with
UNIQUE(because of the nulls), nor withCHECK(it can't look at other rows), and which is solved with a partial unique indexWHERE return_date IS NULL. - 15 queries ordered by difficulty, each with its technique and its lesson: from the simple filter to the anti-join, from
HAVINGto the conditional aggregate, from the correlated subquery toLATERAL, and from the gapless time series to the pivot, the window and the recursive CTE. With five cross-cutting conditions: a comment with the definition, no lost rows, a tie-breaker in rankings, the result attached and not oneSELECT *. - Performance: indexes on the FKs you navigate, IR-03's partial one, one for the time series and another for title search, each justified with the query that uses it, two commented
EXPLAINs and the list of what you decide not to index. - Security: three least-privilege roles, no
DELETEon the history, reports served by aggregate views, soft account closure with anonymisation planned for, and entirely fictitious data. - Delivery: four files that run in order on an empty database, an idempotent schema, and the rubric of six criteria —model 25 %, integrity 20 %, queries 25 %, performance 10 %, security 10 %, delivery 10 %— with four failures that wipe out their whole block.
You know what has to be done and what you'll be measured against. What's missing is the how. In the next lesson, Project Implementation, you'll find the seven-step build guide: from the brief to the complete entity-relationship diagram, with the hard decisions justified —why copies is a table, why due_date is stored instead of computed, why fines aren't a column and how a queue is modelled—; the commented 01-schema.sql with the partial index explained in depth; how to generate coherent data and what edge cases it must contain; the method for writing queries without going wrong, with two worked examples; how to decide indexes from the queries and not the other way round; what to encapsulate in views, procedures and triggers without overdoing it; and the work schedule.
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
