Ran Wei/CS Series/12
中文
Computer Science Fundamentals — Ran Wei

Module 12: Databases

Model catalogue records relationally, query them with SQL and preserve invariants with constraints and transactions.

≈ 5 hours4 sessions2 labs8 exercises6 quiz questions

By the end you can

  • Design tables and keys.
  • Query and join with SQL.
  • Avoid duplicated facts.
  • Explain index tradeoffs.
  • Use atomic transactions and parameterised values.

Before you start

Recommended modules: 03, 10, 11.

Know records and shared-state updates. SQLite is included with normal Python installations; the labs use temporary databases or memory.

Contents

Study plan

5 hours

Four 75-minute sessions including practice. Allow longer for extensions or unfamiliar prerequisites. Progress is saved locally and shared between language editions.

Session 175 min
Concepts and worked examples
Session 375 min
Apply and extend
1

Relations, keys and constraints

A relational schema describes tables, columns and constraints. A primary key identifies a row; a foreign key refers to a key in another table. An author table plus book.author_id represents ownership without repeating the author's name everywhere. NOT NULL rejects missing required values, UNIQUE rejects duplicates and CHECK enforces local predicates. SQL tables are not ordered sequences: add ORDER BY when order matters. SQLite foreign-key enforcement must be enabled for each connection with PRAGMA foreign_keys=ON before relying on it.

Check your understanding

Does SELECT without ORDER BY promise insertion order?

Worked solution

No; specify ordering explicitly.

2

Queries, joins and aggregation

SELECT chooses columns, WHERE filters rows and JOIN combines rows satisfying a relationship. JOIN books to authors on author_id=id, not on coincidentally equal names. COUNT counts rows; GROUP BY forms groups before aggregation. NULL represents missing or unknown information, so use IS NULL rather than =NULL. A join can multiply rows when multiple matches exist: inspect key cardinalities before assuming one output per book. Parameter placeholders bind data values; they are not substitutes for arbitrary table or column names.

Check your understanding

Why join on author IDs rather than author names?

Worked solution

Names need not be unique or stable.

3

Normalisation and modelling

Repeated facts invite update anomalies: changing one author's name in many book rows can leave contradictory values. Store the author fact once and refer to it. A many-to-many book-author relationship needs a linking table with a composite key, not a comma-separated field. Model editions and physical copies separately if their identities differ. Normalisation helps consistency, but practical denormalisation may improve a measured workload if the update policy preserves correctness. Begin with the facts and dependencies, not the desired screen layout.

Check your understanding

How should multiple authors per book be represented?

Worked solution

A linking table of book_id and author_id pairs.

4

Indexes and query plans

An index is an additional lookup structure maintained alongside table data. It can reduce exact and range lookup work but consumes space and adds update cost. The database chooses a plan based on available structures and statistics; do not assume a declared index is used for every predicate. EXPLAIN QUERY PLAN shows the chosen access strategy. An index on title supports our exact title lookup; a leading-wildcard substring query may still scan. Measure the actual workload and include write costs before adding many indexes.

Check your understanding

Why not index every column automatically?

Worked solution

Indexes cost storage and maintenance and may not help the workload.

5

Transactions and safe queries

A transaction groups changes so they commit together or roll back. Atomicity prevents a loan existing without its stock update; isolation controls interactions between concurrent transactions; durability concerns committed data surviving failures under the database's guarantees. Our conditional UPDATE decreases stock only when copies>0, and the loan insert belongs to the same transaction. Bind user values with ? placeholders so input remains data instead of SQL syntax. Input validation and query parameterisation are complementary; neither replaces authorisation.

Check your understanding

If loan insertion fails after decrement, what must happen?

Worked solution

Roll back the decrement as part of the same transaction.

6

Common misconceptions

  • Connection context management handles transactions, not automatically connection closure.
  • Uniqueness and indexes are related but not interchangeable requirements.
7

Lab setup

Download each script and run it in a terminal with Python 3.11 or later: python m12_database.py. On Windows, py -3 is an alternative; on some systems use python3. The labs use only the standard library. Predict the result before running, then complete the variations. Run without -O so assertions remain enabled. Outputs below were captured by the builder. Code and output are identical in both language editions.

8

Lab 1 — Persist and query

Create an author/book schema, join it, inspect an index and reopen the file to verify persistence across connections.

Download m12_database.py

"""Persist records, join relations and inspect an indexed query."""
import sqlite3, tempfile
from pathlib import Path
with tempfile.TemporaryDirectory() as folder:
    path = Path(folder) / "catalogue.sqlite"
    db = sqlite3.connect(path)
    try:
        db.execute("PRAGMA foreign_keys = ON")
        db.executescript("""
            CREATE TABLE authors(id INTEGER PRIMARY KEY, name TEXT NOT NULL);
            CREATE TABLE books(id INTEGER PRIMARY KEY, title TEXT NOT NULL, author_id INTEGER NOT NULL REFERENCES authors(id));
            CREATE INDEX book_title ON books(title);
        """)
        with db:
            db.execute("INSERT INTO authors VALUES (?, ?)", (1, "Frank Herbert"))
            db.execute("INSERT INTO books VALUES (?, ?, ?)", (1, "Dune", 1))
        query = "SELECT books.title, authors.name FROM books JOIN authors ON authors.id=books.author_id WHERE books.title=?"
        print("joined:", db.execute(query, ("Dune",)).fetchall())
        plan = db.execute("EXPLAIN QUERY PLAN SELECT id FROM books WHERE title=?", ("Dune",)).fetchall()
        indexed = any("INDEX" in row[3] for row in plan)
        print("index used:", indexed); assert indexed
        attack = "' OR 1=1 --"
        assert db.execute("SELECT id FROM books WHERE title=?", (attack,)).fetchall() == []
    finally:
        db.close()
    reopened = sqlite3.connect(path)
    try:
        print("reopened rows:", reopened.execute("SELECT COUNT(*) FROM books").fetchone()[0])
        assert reopened.execute("SELECT COUNT(*) FROM books").fetchone()[0] == 1
    finally:
        reopened.close()
Captured output
joined: [('Dune', 'Frank Herbert')]
index used: True
reopened rows: 1
  1. Add another book for the same author.
  2. Try an invalid author_id.
  3. Run a grouped book count per author.
Worked solution

Reuse author_id=1; invalid foreign IDs raise IntegrityError. GROUP BY author_id with COUNT(*) returns two after insertion. An outer join is needed to include authors with zero books.

9

Lab 2 — Atomic borrowing

A duplicate request fails after an attempted decrement, which must roll back. The lab rejects duplicates; Module 14 implements idempotent replay instead.

Download m12_transactions.py

"""A stock decrement and loan creation commit together or roll back together."""
import sqlite3
db = sqlite3.connect(":memory:")
db.execute("PRAGMA foreign_keys=ON")
db.executescript("""
 CREATE TABLE books(id INTEGER PRIMARY KEY, copies INTEGER NOT NULL CHECK(copies>=0));
 CREATE TABLE loans(request_id TEXT PRIMARY KEY, book_id INTEGER REFERENCES books(id));
 INSERT INTO books VALUES(1, 2);
""")
def borrow(request_id, book_id):
    with db:
        changed = db.execute("UPDATE books SET copies=copies-1 WHERE id=? AND copies>0", (book_id,)).rowcount
        if changed != 1: raise ValueError("unavailable")
        db.execute("INSERT INTO loans VALUES (?, ?)", (request_id, book_id))
try:
    borrow("request-a", 1)
    try:
        borrow("request-a", 1)
    except sqlite3.IntegrityError:
        print("duplicate request rolled back")
    assert db.execute("SELECT copies FROM books").fetchone()[0] == 1
    borrow("request-b", 1)
    try:
        borrow("request-c", 1)
    except ValueError:
        print("out of stock rejected")
    print("copies:", db.execute("SELECT copies FROM books").fetchone()[0], "loans:", db.execute("SELECT COUNT(*) FROM loans").fetchone()[0])
    assert db.execute("SELECT COUNT(*) FROM loans").fetchone()[0] == 2
finally:
    db.close()
Captured output
duplicate request rolled back
out of stock rejected
copies: 0 loans: 2
  1. Remove transaction grouping and identify the broken invariant.
  2. Test an unknown book.
  3. Explain why a conditional UPDATE is better than an unprotected check followed by decrement.
Worked solution

Committing the decrement before a failed insert loses inventory without a loan. Unknown books affect zero rows and are rejected. Conditional mutation combines the availability predicate and update, avoiding a stale separate read.

10

Exercises with worked solutions

Try before opening the solution. ★ applies an idea; ★★ combines ideas; ★★★ asks for design or proof.

Exercise 1 — Keys★

Distinguish primary and foreign keys.

Worked solution

A primary key identifies a row; a foreign key references another candidate key and enforces the relationship when enabled.

Exercise 2 — Ordering★

How do you request books sorted by title then ID?

Worked solution

SELECT id,title FROM books ORDER BY title,id.

Exercise 3 — Missing values★★

Why is title=NULL not the null check?

Worked solution

NULL participates in unknown-valued comparisons; use title IS NULL.

Exercise 4 — Many-to-many★★

Design authorship for books with several authors.

Worked solution

book_authors(book_id,author_id) with a composite primary key and foreign keys to both entities.

Exercise 5 — Injection★★

How should a quoted title be passed to SQL?

Worked solution

execute('SELECT id FROM books WHERE title=?',(title,)); do not interpolate it into the SQL text.

Exercise 6 — Invariant★★★

State the borrowing stock invariant.

Worked solution

For each book, available copies plus active loans equals total copies, assuming no additions/returns during the operation. Changes must preserve it atomically.

Exercise 7 — Failure recovery★★★

A transaction decrements then violates UNIQUE. Final state?

Worked solution

If the exception leaves the transaction context and triggers rollback, both changes are undone and the earlier committed state remains.

Exercise 8 — Index tradeoff★★

Why may adding a title index slow imports?

Worked solution

Each insert must also maintain the index, adding work and storage writes.

11

Self-check quiz

Choose an answer for feedback; reset to retry. A text answer key is available without JavaScript.

1

A table has guaranteed output order?

2

SQLite foreign keys should be?

3

A transaction failure should?

4

Parameter placeholders protect?

5

An index costs?

6

Unknown value check?

Answer key
  1. A — ORDER BY makes the requirement explicit.
  2. B — Enable before depending on enforcement.
  3. C — Atomicity rules out partial application.
  4. A — They are not a permission system.
  5. B — Lookup benefits have tradeoffs.
  6. C — NULL differs from empty text.
12

Guided reading

13

Review and the next step

Draw the author/book schema and explain a failed loan transaction. Module 13 explores how programs themselves become structured input and where computation has limits.

14

Key terms

TermMeaning
Primary keyA unique row identifier.
TransactionA grouped operation with commit/rollback semantics.
NormalisationOrganising facts around dependencies to reduce anomalies.