Designing Relational Database Tables
Loading relational data is not always a matter of inserting one complete source record into one table.
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…
Why One Record Needs Several Steps
Loading relational data is not always a matter of inserting one complete source record into one table. A source record may contribute data to multiple linked tables. When a later table needs a foreign key, the loader must first ensure that the referenced parent row exists and then retrieve that row's ID. The INSERT OR IGNORE and SELECT pattern provides these two operations in sequence.
The central idea is: insert the parent record or ignore it if it already exists, select its ID, and use that ID when inserting the related child record.
What do you think happens?
If a parent record already exists, what should the loader do before inserting a child record that refers to it?
Reveal answer
Ignore the duplicate insertion, then select the existing row's ID
INSERT OR IGNORE skips a duplicate value rather than creating another row. The following SELECT retrieves the ID of the row that can be used as the foreign key in the next insertion.
The Insert-and-Select Sequence
- 1Attempt to insert the parent record with INSERT OR IGNORE.
- 2If the record is a duplicate, the duplicate insertion is skipped instead of creating another row.
- 3Run SELECT to retrieve the ID of the relevant row, whether it was newly inserted or already existed.
- 4Use the retrieved ID as the foreign key when inserting the related child record.
- 5When the SELECT result is returned as a tuple, use
cur.fetchone()[0] to extract the actual ID value.
INSERT OR IGNORE is important because a source load can encounter the same parent value more than once. The operation handles the duplicate gracefully by skipping the duplicate insertion. The SELECT that follows is still necessary: the child insertion needs the ID of the existing parent row, not merely confirmation that a duplicate was found.
Following Foreign Key Dependencies
Insertion order follows the direction of the foreign key dependency. A parent table must be populated before a child table that references it. The parent row's ID is retrieved first, then supplied to the child insertion. If another table depends on that child, the same reasoning continues: establish the referenced row before inserting the row that points to it.
Before writing a load operation, identify which tables provide IDs and which tables store those IDs as foreign keys. Build the load order from those dependencies: parent tables first, followed by the child tables that reference them.
Tracing a Source Record
Consider this generated scenario: a source record contains information that belongs in a parent table and also information that belongs in a child table. The child table stores a foreign key pointing to the parent. The loader cannot safely complete the child insertion until it has obtained the parent's ID.
A Parent ID Used by a Child
A generated source record refers to one parent value and one related child value. Describe the load sequence without creating duplicate parent rows.
- 1Establish the parent
Use INSERT OR IGNORE for the parent value. If the value is new, its row is inserted. If it already exists, the duplicate insertion is skipped.
- 2Find the parent ID
Use SELECT to retrieve the ID of the parent row. Because
cur.fetchone()returns a tuple, usecur.fetchone()[0] to obtain the ID itself. - 3Insert the child
Pass the retrieved parent ID into the child insertion as the foreign key. The child is inserted only after its referenced parent row has been established.
- 4Repeat for further dependencies
If another related table depends on the child, retrieve the needed ID and continue in dependency order.
The source record is loaded across linked tables without duplicating the parent value and with the child foreign key pointing to an actual parent row.
Mistakes That Break the Load
Common mistake Inserting the child before the parent
What it looks likeThe loader attempts to insert a child row before the referenced parent row has been inserted or found.
Why it failsThe child depends on the parent's ID through a foreign key.
Do this insteadInsert or ignore the parent first, retrieve its ID, and then insert the child.
Common mistake Treating INSERT OR IGNORE as the complete operation
What it looks likeThe loader ignores a duplicate parent insertion but does not run SELECT afterward.
Why it failsThe child insertion still needs the existing parent's ID.
Do this insteadAlways follow the insert-or-ignore step with a SELECT that retrieves the required ID.
Common mistake Using the entire fetch result instead of the ID
What it looks likeThe loader passes cur.fetchone() directly as the foreign key value.
Why it failscur.fetchone()returns a tuple, while the actual ID is the first value in that tuple.Do this insteadUse cur.fetchone()[0] to extract the actual ID.Common mistake Creating duplicate parent rows for repeated source values
What it looks likeThe loader inserts every occurrence of a parent value without duplicate handling.
Why it failsRepeated source data can produce duplicate values instead of reusing the existing row.
Do this insteadUse INSERT OR IGNORE so duplicate values are skipped, then retrieve the existing row's ID.
Practice the Dependency Trace
A source file contains repeated values that belong to a parent table and related values that belong to a child table. Describe the exact order of operations for one source record. Include what happens when the parent value already exists, how the parent ID is extracted, and where that ID is used.
Hints
- Start with the table that provides the ID.
- Place INSERT OR IGNORE before SELECT.
- Remember that
cur.fetchone()[0] extracts the ID from the returned tuple. - The child insertion uses the retrieved ID as its foreign key.
- 1Identify the parent table and the child table that references it.
- 2Place the parent insertion before the child insertion.
- 3Use INSERT OR IGNORE for the parent value.
- 4Use SELECT to retrieve the parent row's ID.
- 5Extract the ID with
cur.fetchone()[0]. - 6Use that ID as the child table's foreign key.
Reliable Multi-Table Loading
- 1INSERT OR IGNORE skips duplicate values instead of creating duplicate rows.
- 2SELECT immediately after the insertion attempt retrieves the ID of the new or existing parent row.
- 3
cur.fetchone()[0] extracts the actual ID from the tuple returned by SELECT. - 4Parent tables must be populated before child tables that reference them through foreign keys.
- 5The retrieved parent ID is the bridge that carries one source record into the next related table.
Key Takeaways
- The INSERT OR IGNORE and SELECT pattern handles duplicates and retrieves the ID needed for related insertions.
- A parent row must be inserted or found before a child row can use its ID as a foreign key.
- Use
cur.fetchone()[0] to extract the actual ID from the tuple returned by SELECT. - Tracing parent insertion, ID retrieval, and child insertion makes the flow of a multi-table load understandable.