Relational databases and keys
Flat files against relational databases, and every kind of key: primary, composite, foreign and secondary, with indexes and how records are found.
Do this lesson in the simulatorAt GCSE (F7.2) you met tables, records, fields, primary keys and foreign keys. At A level the vocabulary is more exact, there are more kinds of key, and you need to say how a database finds a record quickly. This lesson turns the entity descriptions of the last lesson into a relational database.
Flat files and relational databases
A flat file database holds all the data in one table, often a single file such as a CSV. Every run record repeats the robot's name, colour and team:
| RunID | Robot | Colour | Team | Task | Cm |
|---|---|---|---|---|---|
| 1 | Ada | green | Hawks | wall | 42.0 |
| 2 | Ada | green | Hawks | square | 80.0 |
| 3 | Bolt | red | Hawks | wall | 38.0 |
A relational database stores the data in several tables, one for each entity, and links them through keys. The name comes from the mathematical word for a table, a relation.
| Flat file | Relational | |
|---|---|---|
| Structure | one table | many linked tables, one per entity |
| Redundancy | the same facts repeated in many records | each fact stored once |
| Consistency | an edit missed in one record leaves the data contradicting itself | one change updates the fact everywhere |
| Queries across entities | awkward, often by hand | joins in SQL |
| Security | usually all or nothing | access can be granted table by table |
| Set-up | simple, needs no DBMS | needs design and a database management system (DBMS) |
A flat file is fine for a small list with one purpose, such as one robot's sensor log. Once the data describes several related things and is shared and changed, a relational database is the better choice.
The vocabulary
| Formal term | Everyday term | Meaning |
|---|---|---|
| Relation | table | a set of records about one entity |
| Tuple | record, row | one instance of the entity |
| Attribute | field, column | one property; every value in a column has the same data type |
Keys
- A candidate key is any attribute, or minimal set of attributes, that could identify each record uniquely.
- The primary key is the candidate key chosen to identify records. Its value must be unique and must never be empty (null). The DBMS enforces both.
- A composite primary key (or compound key) is a primary key made of two or more attributes, where no one attribute is unique on its own but the combination is. A robot makes several attempts, so neither
RobotIDnorAttemptNoidentifies an attempt, but (RobotID,AttemptNo) does. - A foreign key is an attribute in one table that holds the primary key of a record in another table. It is how a relationship is built:
TeamIDin Robot links each robot to its team. - A secondary key is an attribute, other than the primary key, that is indexed so records can be searched for or sorted by it quickly. A robot's
Nameis a good secondary key: people look robots up by name, although two robots could share one.
import sqlite3
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE attempts (robot_id INTEGER, attempt INTEGER, cm REAL, PRIMARY KEY (robot_id, attempt))")
db.execute("INSERT INTO attempts VALUES (1, 1, 42.0)")
db.execute("INSERT INTO attempts VALUES (1, 2, 45.5)") # same robot, new attempt: fine
db.execute("INSERT INTO attempts VALUES (2, 1, 38.0)") # same attempt number, new robot: fine
try:
db.execute("INSERT INTO attempts VALUES (1, 2, 44.0)")
except sqlite3.IntegrityError as e:
print("refused:", e)
print(db.execute("SELECT * FROM attempts").fetchall())
CREATE TABLE builds a table (you met it at GCSE in F7.5, and lesson A11.4 covers it fully). The DBMS refuses the fourth record because the pair (1, 2) already exists, even though 1 and 2 each appear more than once on their own.
Indexes
To find the records where task = 'wall', a database with no help must read every record: a full table scan, O(n). An index is a separate structure that holds the values of one attribute in order, each with the location of its record, so the DBMS can go straight to the matching records. Most DBMSs keep indexes as balanced trees, which makes a lookup about O(log n). The primary key is indexed automatically; an index on any other attribute makes it a secondary key.
import sqlite3
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE runs (run_id INTEGER PRIMARY KEY, task TEXT, cm REAL)")
def plan(sql):
for row in db.execute("EXPLAIN QUERY PLAN " + sql):
print(" ", row[-1])
print("find by primary key:")
plan("SELECT * FROM runs WHERE run_id = 3")
print("find by task, no index:")
plan("SELECT * FROM runs WHERE task = 'wall'")
db.execute("CREATE INDEX idx_runs_task ON runs (task)")
print("find by task, with an index:")
plan("SELECT * FROM runs WHERE task = 'wall'")
EXPLAIN QUERY PLAN shows how SQLite will run a query: SCAN reads every record, SEARCH ... USING INDEX jumps to them. An index is not free. It takes storage, and every INSERT, UPDATE and DELETE must update the index as well as the table. So index the attributes that are searched or sorted often, not every attribute.
How records are found in a file
Underneath, a database is records in files, and there are four classic ways to organise a file of records:
| Organisation | How records are stored | How one record is found | Suits |
|---|---|---|---|
| Serial | in the order they arrive | read from the start until found | logs and transaction files, where records are added at the end |
| Sequential | sorted by key | read from the start, but you can stop once past the key | master files updated in batches |
| Indexed sequential | sorted by key, with an index to groups of records | look up the index, jump near the record, read a few | files used both in batches and for single lookups |
| Direct (hashed) | at an address calculated from the key by a hash function | calculate the address and read it | fast single lookups, such as a booking system |
A hash function turns a key into an address. Two keys can give the same address, a collision, so the file needs a rule for where the second record goes:
SLOTS = 7
table = [None] * SLOTS
for robot_id in [15, 22, 31, 8]:
address = robot_id % SLOTS
while table[address] is not None: # collision: try the next slot
address = (address + 1) % SLOTS
table[address] = robot_id
print("robot", robot_id, "stored at", address)
print(table)
15, 22 and 8 all hash to address 1, so 22 and 8 move on to the next free slots. This is linear probing. You will meet hash tables as a data structure in module A3.
Task: a composite key
A robot may make several attempts at a task, numbered from 1 for each robot. The list attempts holds records (robot_id, attempt, cm): two whole numbers and a float.
- Create a table
attemptswith the fieldsrobot_id,attemptandcm, and the composite primary key (robot_id,attempt). - Insert every record, in order. For each one print
added robot <robot_id> attempt <attempt>, or, if the database refuses it withsqlite3.IntegrityError, printrejected robot <robot_id> attempt <attempt>: already logged. - Use
SELECT COUNT(*)to print the number of records now in the table asrecords: <n>.
Seven lines in all. The robot does not move.
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
import sqlite3
attempts = [(1, 1, 42.0), (1, 2, 45.5), (2, 1, 38.0), (1, 2, 44.0), (3, 1, 76.5), (2, 1, 39.0)]
db = sqlite3.connect(":memory:")
Challenges
- The Booking table has the key (StudentID, RobotID, SessionDate). Could a student book two robots on the same day? Could they book the same robot twice on one day?
- Store 1,000 random run records, then time
SELECTby task with and without an index. How big must the table be before the index shows? - A car park system looks up a car by its registration. Which file organisation would you choose, and why?