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.

A11.2Databases and big dataA level55 min

Covers OCR H446 1.3.2 Databases · AQA 7517 4.10.2 Relational databases · Eduqas A500QS 2.4 Organisation and structure of data · Eduqas A500QS 2.5 Databases and distributed systems

Do this lesson in the simulator

At 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.

The flat file as three tables, Run, Robot and Team, each foreign key pointing to the table it links toRunRunIDRobotID*TaskCm11wall42.021square80.032wall38.0RobotRobotIDNameColourTeamID*1Adagreen12Boltred1TeamTeamIDTeamName1Hawkseach foreign key points to the record it links to* foreign key; underlined: primary key
The flat file above as three linked tables. Each foreign key holds the key of a record in the next table. Hawks, written 3 times in the flat file, is now stored once, and Ada's colour once.
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 RobotID nor AttemptNo identifies 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: TeamID in 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 Name is a good secondary key: people look robots up by name, although two robots could share one.
Labelled keys: RobotID primary key, Name secondary key, TeamID foreign key; attempts has the composite primary key robot_id and attemptRobotRobotIDNameColourTeamID*1Adagreen12Boltred1primary keyforeign key, to Teamsecondary key:indexed for searchingattemptsrobot_idattemptcm1142.01245.52138.0composite primary key1 and 2 repeat on their own,but no pair appears twice
The kinds of key in place. RobotID and the pair (robot_id, attempt) are primary keys, TeamID is a foreign key, and Name is a secondary key: it is not unique, but it is indexed so look-ups by name are fast.
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())

Run this in the simulator

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.

SQLite has one exception to the rule that a primary key is never null. For historical reasons it lets a key field hold null, even twice over, unless the key is a single INTEGER PRIMARY KEY or the field is also declared NOT NULL. Other DBMSs refuse it, and exam answers should assume they do.

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.

An index on task, sorted, each entry pointing to its record in the runs table, which is in the order the runs arrivedindex on task (sorted)taskrecordline7square2square5square6wall1wall3wall4runs table (arrival order)run_idtaskcm1wall42.02square80.03wall38.04wall45.55square76.56square81.07line60.0scan: 7 readstask = 'wall': jump to the 3 entries, then to records 1, 3, 4
The index holds every task in order, each with where its record is. To find the wall runs the DBMS goes straight to the 3 wall entries and follows them. Without it, a scan reads all 7 records.
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'")

Run this in the simulator

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)

Run this in the simulator

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.

A hash table of 7 slots after each of the keys 15, 22, 31 and 8 is stored, with the slots each key had to skip0123456address15 % 7 = 115stored at 122 % 7 = 115221 taken, stored at 231 % 7 = 3152231stored at 38 % 7 = 115223183 taken, stored at 4
Each row is the table after one more key. 15, 22 and 8 all hash to 1. 22 and 8 find it taken, a collision, so they step on (red arrows) to the next free slot. 8 has to skip three taken slots, which is why a table that is filling up gets slower.

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.

  1. Create a table attempts with the fields robot_id, attempt and cm, and the composite primary key (robot_id, attempt).
  2. Insert every record, in order. For each one print added robot <robot_id> attempt <attempt>, or, if the database refuses it with sqlite3.IntegrityError, print rejected robot <robot_id> attempt <attempt>: already logged.
  3. Use SELECT COUNT(*) to print the number of records now in the table as records: <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

  1. 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?
  2. Store 1,000 random run records, then time SELECT by task with and without an index. How big must the table be before the index shows?
  3. A car park system looks up a car by its registration. Which file organisation would you choose, and why?