Primary Keys and Unique Constraints
The central pattern is a two-step sequence: INSERT OR IGNORE a row, then SELECT its primary key. That retrieved key becomes the foreign key used by the next related insert.
Try it: Keys and Constraints
How SQLite enforces PRIMARY KEY, UNIQUE, NOT NULL and FOREIGN KEY constraints on INSERT and DELETE, how an INTEGER PRIMARY KEY assigns ids (with and without AUTOINCREMENT), what INSERT OR IGNORE skips, and what ON DELETE RESTRICT, CASCADE and SET NULL do to child rows.
How it works
- INSERT: a NULL id is replaced by max(id) + 1 — or, with AUTOINCREMENT, by one more than the largest id ever used — and last_insert_rowid() reports it.
- Constraints are checked in order: NOT NULL, then the PRIMARY KEY, then UNIQUE, then the FOREIGN KEY (the parent row must exist). The first failure aborts the statement with SQLite's error message.
- INSERT OR IGNORE silently skips a row that breaks NOT NULL, PRIMARY KEY or UNIQUE — but a foreign-key failure is still an error.
- DELETE of a parent row that has children: RESTRICT refuses, CASCADE deletes the children too, SET NULL clears their foreign key. None of this happens unless PRAGMA foreign_keys = ON (SQLite's default is OFF).
Default run (5 steps): 3 artists and 3 albums. PRAGMA foreign_keys = ON, ON DELETE RESTRICT. Choose an operation. Example: INSERT INTO artist (name) VALUES ('Queen'). … Error: UNIQUE constraint failed: artist.name. Nothing was changed (the statement is rolled back).
Simplified: A toy relational engine in the browser, not SQLite itself: a small structured query model built from the controls (no SQL text is parsed or executed), typed INTEGER/TEXT/NULL values, and tables of at most a few rows. Results were checked against Python's sqlite3 (SQLite 3.49). Two fixed tables of at most 8 rows and ids up to 99; changing the schema recreates the tables with the starting rows. Error texts and ids were checked step by step against sqlite3.
Loading the simulation…
Try it: Normalising into Lookup Tables
How normalisation removes repeated text: each distinct artist or album is stored once in a lookup table with an integer id (INSERT OR IGNORE, then SELECT id), the other table stores only that id as a foreign key, and a JOIN rebuilds the original rows. A roster preset links users and courses through a junction table.
How it works
- For each flat row, INSERT OR IGNORE the repeated text into its lookup table; the UNIQUE column makes a second copy be ignored.
- SELECT the text's id back — new or existing.
- Store that integer id (a foreign key) in the next table instead of the text.
- Many-to-many: a junction table stores (user_id, course_id) pairs plus the role; INSERT OR REPLACE keeps one row per pair.
- A JOIN that follows the ids rebuilds the same rows, while each distinct string is now stored once.
Default run (27 steps): A flat table: 6 rows × 3 text columns = 18 stored strings, many of them repeated. Split it into Artist, Album and Track. … Done: 18 stored strings became 12 (179 → 130 characters). The JOIN rebuilt 6 rows, the same rows as the flat table.
Simplified: A toy relational engine in the browser, not SQLite itself: a small structured query model built from the controls (no SQL text is parsed or executed), typed INTEGER/TEXT/NULL values, and tables of at most a few rows. Results were checked against Python's sqlite3 (SQLite 3.49). Follows the Python for Everybody tracks and roster programs on at most 8 rows; the stored-string count ignores the (small) space integers take.
Loading the simulation…
The Linking Problem
Suppose a program reads music records from a CSV file. Each record contains an artist, an album, and a track. The database stores these details in separate tables, so the program must do more than insert text. It must discover the primary key for the artist, use that value when inserting the album, then discover the album's primary key before inserting the track.
The central pattern is a two-step sequence: INSERT OR IGNORE a row, then SELECT its primary key. That retrieved key becomes the foreign key used by the next related insert.
Following One Record
What do you think happens?
A CSV line contains an artist, an album, and a track. Which key must the program obtain before it can insert the album?
Reveal answer
The artist primary key
The Album row contains an artist_id foreign key. The program therefore inserts or finds the Artist first, retrieves its primary key, and uses that value in the Album insert.
From Artist Name to Track Link
Trace the key values needed to connect one artist, one album, and one track.
- 1Read the input
The program reads a CSV file line by line, splits each line into fields, and extracts the artist name.
- 2Handle the Artist
The program executes INSERT OR IGNORE for the Artist and then immediately retrieves that Artist row's primary key.
- 3Store artist_id
The retrieved primary key is stored as
artist_id. This is the concrete value required when inserting the related Album. - 4Handle the Album
The program inserts the Album with
artist_idas its foreign key, then selects and stores the Album's primary key asalbum_id. - 5Handle the Track
The Track insert uses
album_id, linking the Track to its Album. The Album already links to the Artist throughartist_id.
The relationship is built in two links: Track points to Album through album_id, and Album points to Artist through artist_id.
Dependency-Driven Insertion
The insertion order is determined by foreign key dependencies, not by preference. Artist must come first because Album needs artist_id. Album must come second because Track needs album_id. Track comes last because its relationship can be completed only after the Album key has been retrieved.
| Table | Key used from an earlier table | Why it must occur at this stage |
|---|---|---|
| Artist | None in this chain | Its primary key is needed by Album |
| Album | artist_id | Its primary key is needed by Track |
| Track | album_id | It completes the chain to Album and Artist |
The dependency chain determines the population order.
Primary Keys and Repeated Rows
A primary key is the value used to identify a row for relational linking. In this workflow, the Artist primary key is retrieved and stored as artist_id, while the Album primary key is retrieved and stored as album_id. Those values are then stored in related tables as foreign keys.
INSERT OR IGNORE followed by SELECT is useful when the row may already exist. The insert step attempts to place the row in the table while ignoring an existing matching row. The following SELECT retrieves the primary key of the row that should be used for the next relationship. The source material presents this as a two-step pattern for obtaining the correct key before a related insert.
| Idea | Role in this workflow |
|---|---|
| Primary key | The row value retrieved after insertion or recognition and passed into a related table |
| Foreign key | A primary key value from one table stored in another table to establish a relationship |
| INSERT OR IGNORE | The first step when a row may already exist |
| SELECT | The immediate second step that retrieves the primary key for later linking |
The supplied workflow emphasizes how keys connect tables and how the two-step pattern retrieves them.
Joining the Stored Data
The three tables are stored separately to avoid duplication and maintain consistency. A complete music record is therefore not necessarily stored as one row. When the program needs a Track together with its Album title and Artist name, a JOIN combines rows from the separate tables.
A JOIN matches primary and foreign key values. For example, Album.artist_id = Artist.id connects each Album row to its corresponding Artist row. Extending the same relationship through Track.album_id connects a Track to its Album and then to the Artist associated with that Album.
Following a JOIN Across Three Tables
Explain how a complete Track record can include the track data, its album title, and its artist name even though the information is stored in separate tables.
- 1Start with Track
The Track row contains
album_id, which identifies the related Album row. - 2Match the Album
The JOIN matches Track.album_id with the Album primary key. This supplies the Album row and its title.
- 3Follow artist_id
The Album row contains
artist_id. A second JOIN matches Album.artist_id with Artist.id. - 4Return combined data
The result can contain the Track information together with the Album title and Artist name.
JOIN reconstructs a complete view by matching the stored primary and foreign key values.
Track Updates and Existing Data
The source workflow treats Track differently from Artist and Album. Track uses INSERT OR REPLACE rather than INSERT OR IGNORE. This permits the code to update Track metadata such as rating, count, and length when the same track appears again in the CSV.
Common Ordering Mistakes
Common mistake Inserting the Album before the Artist
What it looks likeThe Album insert has no valid artist_id because the Artist row has not been inserted or found yet.
Why it failsAlbum depends on the Artist primary key through its
artist_idforeign key.Do this insteadInsert or ignore the Artist first, select its primary key, and use that value in the Album insert.
Common mistake Inserting the Track before the Album
What it looks likeThe Track insert is attempted before album_id has been retrieved.
Why it failsTrack depends on the Album primary key through
album_id.Do this insteadComplete the Album insert and select its primary key before inserting the Track.
Common mistake Using a name instead of the key value
What it looks likeThe Album stores an artist name rather than the Artist row's primary key.
Why it failsThe described relationship is maintained by matching primary and foreign key values. The Album's
artist_idis not a copy of the artist's name.Do this insteadStore the Artist primary key in artist_id.
Common mistake Stopping after INSERT OR IGNORE
What it looks likeThe program inserts or recognizes the Artist but never selects its primary key.
Why it failsThe next Album insert still lacks the concrete
artist_idneeded for linking.Do this insteadTreat INSERT OR IGNORE and SELECT as one two-step pattern.
Practice the Dependency Chain
A program has just read one CSV line containing an artist name, an album title, and a track. Write the sequence of database actions in the correct order. Then state which key is stored in Album and which key is stored in Track.
Hints
- Begin with the table whose primary key is needed by Album.
- After each Artist or Album insert, retrieve the row's primary key before moving on.
- Album stores
artist_id, while Track storesalbum_id.
Explain how a JOIN can return a Track together with its Album title and Artist name. Identify both key matches that the database follows.
Hints
- Start with Track.album_id.
- Then follow the Album row's
artist_id. - The source relationship matches Album.artist_id with Artist.id.
Key Takeaways
- 1Use INSERT OR IGNORE followed immediately by SELECT when you need the primary key of a row that may already exist.
- 2Populate the tables in dependency order: Artist, then Album, then Track.
- 3Store the Artist primary key as Album.artist_id and the Album primary key as Track.album_id.
- 4Foreign keys connect rows by storing primary key values from related tables.
- 5Use JOIN clauses to combine the separate Artist, Album, and Track data into a complete record.
At a glance
- The INSERT OR IGNORE followed by SELECT pattern retrieves the primary key needed for the next relational insert.
- Foreign key dependencies require the population order Artist, Album, then Track.
- Album stores
artist_id, and Track storesalbum_id, creating a chain of relationships. - JOIN clauses follow those key matches to reconstruct complete records from separate tables.
- Track uses INSERT OR REPLACE in the described workflow when repeated CSV data should update track metadata.