Skip to content
Back to Modules
Part IA Michaelmas Term

Databases

Databases Relational-Model ER-Diagrams SQL Relational-Algebra Normalisation ACID Transactions Document-Databases NoSQL Graph-Databases Neo4j Cypher Part IA Michaelmas Term Paper 3
  • Introduction to Databases

    What a DBMS is, fields and records, database terminology, abstractions and the narrow waist model, storage and persistence, the three data models (relational, document, graph), and an overview of CAP and BASE

    • What is a Database Management System?

      Definition

      A Database Management System (DBMS) is software that provides an abstraction over secondary storage (disks, SSDs). It hides the details of how data is stored on physical media behind a standardised interface (e.g., SQL) that can be shared by many concurrent applications.

      The narrow waist model

      The DBMS sits at a “narrow waist” in the software stack:

      The "Narrow Waist" DBMS Model: decouples many diverse applications from many diverse implementations through a common database interface

      • Above the interface: diverse applications (web apps, transaction processing, analytics, reporting tools) — all written to the same API.
      • Below the interface: diverse storage engines, file formats, indexing strategies, and hardware — all interchangeable without changing the applications.

      This arrangement decouples application development from implementation details. The application programmer writes queries against a logical schema; the DBMS handles the physical storage.

      CRUD operations

      Every DBMS provides four fundamental operations on data:

      OperationMeaning
      CreateInsert new records into the database
      ReadQuery and retrieve existing data
      UpdateModify existing records
      DeleteRemove records from the database

      Management operations

      Beyond CRUD, a DBMS typically offers management-level operations:

      • Create or alter the schema (tables, columns, types, constraints)
      • Create views (virtual tables for access control or convenience)
      • Physical reorganisation of stored data
      • Re-indexing for query performance
      • Backup and recovery
      • Statistics generation for query planning

      These management operations are mostly beyond the scope of the Part IA course. The course focuses on the application writer’s point of view: data models and query languages.

      Why use a DBMS?

      Without a DBMSWith a DBMS
      Each application manages its own filesAll applications share a common interface
      Concurrency must be handled manuallyDBMS manages concurrent access
      No standard query languageStandardised SQL
      Storage format changes break applicationsPhysical/logical independence
      Inconsistent or corrupt data is commonACID guarantees

      Summary

      • A DBMS provides an abstraction layer between applications and physical storage.
      • The narrow waist model gives both application portability and implementation flexibility.
      • The core data operations are Create, Read, Update, Delete.
      • This course studies the DBMS from the application writer’s perspective: modelling data and writing queries.
    • Fields, Records, and Data Representation

      From punched cards to key-value stores

      Fixed-field records were the earliest digital data format, inherited from punched cards. Each field occupies a predetermined range of character positions on a line. This is efficient for machines to parse but inflexible: changing a field width requires rewriting every record.

      Comma/character-separated value (CSV) records use delimiters to separate fields. This is more flexible than fixed-width, but still imposes a simple row-and-column structure and struggles with embedded delimiters or line breaks within field values.

      The in-core associative store

      The simplest possible database is an in-core associative store — a dictionary (or collection) held in primary memory with just two operations:

      store(key, value)
      retrieve(key)

      There are no constraints on the order of invocations. The associative store can be implemented with a hash table, balanced tree, or any other dictionary data structure. It is ephemeral (lost when the process exits) and limited by the size of RAM.

      Database terminology

      Students are expected to understand all of the following glossary terms by the midpoint and endpoint of the course:

      TermDefinition
      ValueAn individual piece of data: a character string, number, date, or even a polygon in a spatial database
      Field (attribute, column)A named category of values; e.g., surname, date_of_birth
      Record (row, tuple)A sequence of fields that together describe one entity
      SchemaA specification of how data is arranged: table names, field names, data types, and consistency rules
      KeyA field (or concatenation of fields) used to uniquely locate a record
      IndexA derived data structure that provides fast lookup of records by non-key fields
      QueryA retrieve or lookup operation expressed in a query language
      UpdateA modification to the stored data
      TransactionAn atomic unit of change to the database with ACID properties

      Schemas

      A schema defines the shape of the data:

      Table: Students
        Name:     string, not null
        CRSID:    string, not null, primary key
        College:  string
        Year:     integer, range [1, 4]

      The schema is enforced by the DBMS. Any insert or update that violates a constraint is rejected. This is in contrast to schema-free systems (e.g., document databases), where the application must enforce its own consistency.

      Summary

      • Data can be represented as fixed-field records, delimited records, or in-core dictionaries.
      • The core vocabulary of databases — field, record, schema, key, index, query, update, transaction — underpins everything that follows in the course.
      • A schema is a contract: the DBMS guarantees that all data conforms to it.
    • Abstractions, Interfaces, and the Narrow Waist

      Interfaces as liberation

      An interface is a contract that liberates application writers from low-level details. It represents an abstraction of the resources and services used by applications. In an ideal world, implementations can change without requiring changes to applications.

      In practice, this ideal is often undermined by:

      • Performance concerns: a naive abstraction may perform poorly on a particular implementation, forcing the application writer to become aware of what lies beneath.
      • Mission creep: the interface specification grows over time, adding features that tie applications to specific implementations.
      • Specification change: the interface itself is revised, breaking existing applications.

      Standard interfaces are everywhere

      The narrow waist pattern recurs in many engineered systems:

      SystemInterfaceWhat sits aboveWhat sits below
      National electricity grid230 V, 50 Hz socketAppliancesPower stations, renewables, interconnectors
      Landline telephoneRJ11 jack, dial toneTelephones, fax machines, modemsExchanges, trunks, fibre, microwave links
      MoneyCurrency units, pricesBuyers, sellers, contractsCentral banks, commercial banks, ledgers
      TCP/IPSockets, portsApplicationsEthernet, Wi-Fi, 4G, fibre

      In each case, the interface decouples the consumers of a service from the providers. New providers can enter below the interface; new consumers can enter above it.

      The DBMS logical arrangement

      DBMS Logical Arrangement: layered stack from user applications down through the DBMS, OS file system, drivers, to physical storage

      Applications interact only with the DBMS. The DBMS uses the operating system’s file system, which in turn uses device drivers to access physical storage. Some high-performance DBMSs bypass the file system and manage raw storage devices directly.

      Storage interface primitives

      Primary storage (RAM) provides a simple word-level interface:

      write(address, word)
      read(address)

      Secondary storage (disk/SSD) provides a block-level interface:

      write(blkaddress, block)
      read(blkaddress) → option(block)
      trim(blkaddress)
      sync()
      • read returns an option: the block might not exist (unlike RAM, where every address always contains something).
      • trim tells the device that a block is no longer needed (used by SSDs for garbage collection).
      • sync flushes buffered writes to persistent media.

      Storage configurations

      ConfigurationWhere data livesPersistence
      In-corePrimary memory (RAM)Ephemeral — lost on process exit or power loss
      Serialised to filesystemFiles managed by the OSPersistent across restarts
      Direct device accessRaw secondary storage, bypassing the file systemPersistent, with full DBMS control over layout

      Most general-purpose DBMSs use the serialised-to-filesystem configuration. In-core databases (e.g., Redis) are used where latency is critical and the dataset fits in RAM.

      Summary

      • An interface is an abstraction that decouples consumers from providers.
      • The narrow waist model appears in many engineered systems, not just databases.
      • The DBMS sits between user applications and the file system/device drivers.
      • Secondary storage differs from primary storage in providing block-level access with optional read results.
    • Storage, Consistency, and CRUD

      Storage classifications

      Databases can be classified along three axes:

      By medium

      TypeDescription
      In-coreData held entirely in primary memory (RAM). Fast, but capacity-limited and ephemeral.
      Secondary storageData held on disk or SSD. Persistent, high capacity, but slower access.

      By persistence

      TypeDescription
      EphemeralData disappears when the process exits or the machine loses power.
      PersistentData survives process termination and power cycles.

      By scale

      TypeDescription
      In-core (big data fits)The entire dataset can reside in RAM. Simplifies design.
      Big dataThe dataset exceeds available RAM. Requires careful management of what is in memory and what is on disk.

      Write patterns

      PatternCharacteristicsExample use case
      Read-optimisedData rarely changes; heavy read traffic. Indexes built aggressively.Reference data, product catalogues
      Transaction-optimisedMany concurrent queries and updates. Locking and logging dominate.Banking, ticket booking
      Append-only journalData is never overwritten; only appended. Old state preserved.Audit logs, event sourcing

      Consistency models

      ACID (strong consistency)

      Atomicity, Consistency, Isolation, Durability. Every transaction is an indivisible unit: it either completes entirely or has no effect. After a transaction commits, all subsequent reads see its effects. This is the traditional model in relational DBMSs.

      BASE (relaxed consistency)

      Basically Available, Soft state, Eventual consistency. The system may not be immediately consistent after an update, but if updates cease, all replicas will eventually converge. This model is common in distributed NoSQL systems.

      PropertyACIDBASE
      ConsistencyStrong, immediateEventual
      AvailabilityMay block during partitionsPrioritised
      ComplexityHigher for the DBMSHigher for the application
      Typical useFinancial transactionsSocial media, content delivery

      Data arrangement

      ModelStructure
      RelationalData in tables with rows and columns. Schema enforced.
      Semi-structured (document)Data as nested documents (JSON, XML). Schema flexible or absent.
      GraphData as nodes and edges. Rich support for path-oriented queries.

      Consistency checks

      A DBMS can enforce several kinds of constraint:

      CheckDescriptionExample
      Value rangeA field’s value must fall within a specified domainweight cannot be negative
      Foreign key referential integrityA value referencing another record must point to something that existsA patient’s GP must exist in the Doctors table
      Value atomicityEach field holds one piece of informationDo not cram two street names into a single address field
      Entity integrityEvery row must be identifiableNo row may have a NULL primary key

      Summary

      • Databases are classified by storage medium (in-core vs. secondary), persistence (ephemeral vs. persistent), and scale.
      • Write patterns range from read-optimised to append-only.
      • ACID provides strong guarantees; BASE trades consistency for availability and partition tolerance.
      • Consistency checks — domain, referential, atomicity, entity — protect data quality.
    • Distributed Databases, CAP, and BASE

      Why distribute data?

      A distributed database stores data across multiple machines connected by a network. The motivations are:

      MotivationExplanation
      ScalabilityThe dataset or workload is too large for a single machine. Adding more machines increases capacity.
      Fault toleranceIf one machine fails, the service survives using replicas on other machines.
      Lower latencyData can be located physically closer to its users (e.g., serving European users from a European data centre).

      The cost of distribution: consistency

      After an update is applied to one replica, there is a massive overhead in ensuring that all other replicas reflect the change immediately. If a user reads from a stale replica, they see outdated data.

      There is a multitude of successively-relaxed consistency models that trade stronger guarantees for better performance:

      • Linearizability (strongest, hardest to achieve)
      • Sequential consistency
      • Causal consistency
      • Read-your-writes consistency
      • Monotonic reads
      • Eventual consistency (weakest, easiest to achieve)

      The CAP theorem

      Formulated by Eric Brewer in 2000, the CAP theorem states that a distributed database can provide at most two of the following three guarantees simultaneously:

      GuaranteeMeaning
      ConsistencyAll reads return the most up-to-date data. Every read sees all previous writes.
      AvailabilityAll clients can find some replica of the data, even during failures.
      Partition toleranceThe system continues to operate despite arbitrary message loss or failure of part of the network.

      It is impossible, with current technology, to achieve all three. In practice, network partitions are inevitable, so the real choice is between CP (consistency over availability) and AP (availability over consistency).

      The CAP Theorem Triangle: Consistency (C), Availability (A), and Partition Tolerance (P) tradeoff

      A system can sit anywhere on the edges of this triangle, but not at the centre.

      Approximating CAP is the subject of the Part IB Concurrency and Distributed Systems course.

      BASE: the alternative to ACID

      When a system chooses AP over CP, it adopts the BASE model as an alternative to ACID:

      LetterTermMeaning
      BABasically AvailableThe system prioritises availability over immediate consistency. It will respond to every request, even if the response might be stale.
      SSoft stateStored values may change without application intervention, owing to eventual consistency updates or network partition recovery. The system does not need to be externally updated to change state — it changes by itself as replicas converge.
      EEventual consistencyIf update activity ceases, the system will eventually reach a consistent state. There is no guarantee of when this will happen, only that it will.

      Comparison

      PropertyACIDBASE
      Consistency modelStrong, immediateEventual
      Partition behaviourMay refuse requests (CP)Continues serving (AP)
      Application burdenLow — DBMS handles consistencyHigh — app must tolerate stale reads
      ExamplesPostgreSQL, MySQL (single-node)DynamoDB, Cassandra, CouchDB

      Summary

      • Distribution provides scalability, fault tolerance, and lower latency — at the cost of consistency.
      • The CAP theorem says you can have at most two of Consistency, Availability, and Partition tolerance.
      • BASE (Basically Available, Soft state, Eventual consistency) is the practical alternative to ACID for distributed systems.
    • Three Data Models

      Overview

      The course covers three data models. The relational model has been the industry mainstay for over 48 years; the other two are representatives of the NoSQL movement.

      ModelStructureQuery languageOptimised for
      RelationalTablesSQLHigh throughput of concurrent updates
      Document-orientedNested documents (JSON)Python API, path queriesRead-heavy workloads, semi-structured data
      Graph-orientedNodes and edgesCypherPath-oriented queries, network traversal

      All three primarily hold discrete data. The Lent term MLRD course deals with soft/continuous decision making.

      Relational model

      Data is stored in 2-D tables:

      • Each row is a record (tuple).
      • Each column is a field (attribute).
      • The schema indicates field names, data formats, and which column(s) comprise the key.
      • Row and column ordering is unimportant.
      • SQL is the main query language.

      Example table:

      NameCRSIDCollegeYear
      Aliceabc12King’s2
      Bobdef34Trinity1
      Charlieghi56St John’s3

      The primary key (underlined in schema diagrams) is CRSID.

      The relational model has been the industry mainstay since the 1970s due to its strong theoretical foundation (relational algebra, normalisation) and robust ACID transaction support.

      Document-oriented model

      Also called aggregate-oriented database. Instead of splitting data across normalised tables, related data is stored together as a single document.

      • Optimised for read-oriented databases with few updates.
      • Uses semi-structured data (typically JSON).
      • No rigid schema: different documents can have different fields.
      • Queries are expressed in the host programming language or via path expressions.

      Example document (JSON):

      {
        "name": "Alice",
        "crsid": "abc12",
        "college": "King's",
        "papers": [
          {"code": "CS1", "title": "Foundations of CS"},
          {"code": "CS2", "title": "Software Engineering"}
        ]
      }

      Graph-oriented model

      Data is represented as nodes (entities) and edges (relationships). Both nodes and edges can carry properties.

      • Query languages tend to have path-oriented capabilities.
      • Extensive support for standard graph techniques: shortest path, breadth-first search, PageRank.
      • Well-suited to highly connected data (social networks, recommendation engines, transport networks).

      Example (Cypher):

      CREATE (alice:Student {name: 'Alice', crsid: 'abc12'})
      CREATE (bob:Student {name: 'Bob', crsid: 'def34'})
      CREATE (alice)-[:FRIEND_OF]->(bob)

      Three database systems used in the course

      SystemTypeQuery languageNotes
      SQLiteRelationalSQLOpen-source, embedded, single-file database
      TinyDBDocument-orientedPython APIOpen-source, in-process, no server required
      Neo4jGraph-orientedCypherJava-based, client-server architecture

      Examination

      For examination purposes, students must learn everything in the slide deck and know all glossary terms. A relational database consists of 2-D tables; the schema indicates field names, data formats, and which column(s) comprise the key (conventionally underlined). Row and column ordering is unimportant.

      Summary

      • The three data models are relational (tables + SQL), document-oriented (JSON + path queries), and graph-oriented (nodes/edges + Cypher).
      • The relational model dominates industry; the NoSQL models address specific use cases (semi-structured data, highly connected data).
      • The course uses SQLite, TinyDB, and Neo4j as concrete exemplars.
  • Entity-Relationship Models

    Entities and attributes as the nouns of data modelling, keys and synthetic keys, relationships as verbs, cardinality (many-to-many, many-to-one, one-to-one), ternary relationships, weak entities and discriminators, and entity hierarchies with IsA inheritance

    • Entities and Attributes

      Origins of ER modelling

      Entity-Relationship (ER) diagrams are a conceptual modelling technique introduced by Peter Chen in 1976. They provide an implementation-independent way to describe the data we intend to store in a database. Although ER modelling grew up around relational database systems, it helps clarify design issues for any data model: it forces you to think about what the data means before you commit to tables, columns, or SQL.

      The key insight is that ER diagrams are not tied to a particular DBMS, storage engine, or even the relational model. They capture the conceptual schema — the logical structure of the data as understood by the domain expert — and this schema can later be mapped to a relational schema, a document store, or a graph database.

      Entities

      Entities represent the nouns of our domain — the real-world objects or concepts we want to record information about. In an ER diagram, entities are drawn as rectangles labelled with the entity type name.

      DomainExample entities
      Film databaseMovie, Person, Studio, Genre
      UniversityStudent, Module, Lecturer, Department
      E-commerceCustomer, Order, Product, Supplier

      An entity set is the collection of all instances of a given entity type at a particular moment. For example:

      Movie = {(tt0113189, GoldenEye, 1995),
               (tt0988045, Sherlock Holmes, 2009),
               (tt0371746, Iron Man, 2008)}

      Each member of the entity set is an entity instance (or simply an entity). The data model does not need to enumerate every possible instance; it only specifies the structure that any valid instance must conform to.

      Attributes

      Attributes represent the properties of an entity — the facts we want to record about each instance. In an ER diagram, attributes are drawn as ovals connected to their entity rectangle.

      A Movie entity might have attributes: title, release_year, running_time, budget. Each attribute draws its values from a domain (a set of permissible values). The domain of release_year might be {1888, 1889, ...}; the domain of title might be variable-length strings.

      Keys

      A key is an attribute (or set of attributes) whose value uniquely identifies each entity instance within an entity set. In ER diagrams, key attributes are underlined.

      A key must satisfy two properties:

      1. Uniqueness: no two distinct entities in the set may have the same key value.
      2. Minimality (ideal): no attribute can be removed from the key without destroying uniqueness.

      Keys may be formed algorithmically. A CRSID (e.g., abc123) is a key for Cambridge students: it is assigned by the University and is unique within that namespace. However, relying on keys that lie outside your control is dangerous. A National Insurance number might seem like a convenient natural key, but:

      • You do not issue or manage NI numbers; they can be reassigned under exceptional circumstances.
      • The format could change.
      • Not everyone in your system will have one (e.g., international customers).
      • Data entry errors are irreversible if the key is used as a foreign key throughout the schema.

      Synthetic keys

      In practice, the only safe thing to use as a primary key is a synthetic key (also called a surrogate key): an identifier that is automatically generated by the database and has no meaning outside the database itself. Common examples:

      TypeExampleNotes
      Auto-increment integer1, 2, 3, ...Simple, fast, but reveals insertion order
      UUID (v4)550e8400-e29b-...Globally unique, larger storage cost
      ULID01ARZ3NDEKTSV4R...Time-sortable, URL-safe

      The important principle: a synthetic key is an internal implementation detail. Users of the system should never see it, type it, or depend on it staying the same across database migrations. It exists purely to give each row a stable, unique address within the database.

      Scope of the model

      An ER diagram implicitly declares the scope of the model. Among the infinitely many possible attributes a real-world entity possesses, the diagram selects only those relevant to the application. A Movie has a plot synopsis, a colour grading, a distribution contract — but if your database is a simple viewing tracker, you model only title, year, and genre. The attributes you choose to include (and, just as importantly, to exclude) define the boundary of what your system knows about.

      Summary

      • ER diagrams are a conceptual design tool, independent of any particular DBMS.
      • Entities (rectangles) are the objects we model; attributes (ovals) are their properties.
      • A key (underlined) uniquely identifies each entity instance.
      • Synthetic keys are safer than natural keys because they are under the database’s sole control.
      • The model’s scope is a design decision: you declare which attributes matter for your application.
    • Keys and Synthetic Keys

      What a key does

      A key uniquely identifies each entity instance within an entity set. Given a key value, there is at most one entity with that value. This property is what makes keys the foundation of data integrity: they give us a reliable way to refer to a specific row.

      Natural keys

      A natural key is a key formed from attributes that have real-world meaning — a National Insurance number, an email address, a vehicle registration plate, an ISBN. They seem appealing because they already exist and users already understand them.

      However, natural keys are dangerous for several reasons:

      ProblemExample
      ReassignmentA dissolved company’s VAT number may be reissued to a new entity after several years.
      MutabilityA person changes their email address; an ISBN is reissued for a new edition.
      Non-universalityNot every person has a passport number; not every product has a barcode.
      Format evolutionTelephone numbering plans change; postcode formats are revised.
      Out of your controlYou do not administer NI numbers or domain names. A policy change at the issuing authority can break your schema.
      Privacy concernsUsing an email address or NI number as a primary key exposes it in every foreign key reference, in every join, in every log file.

      Once a natural key is embedded throughout a schema as foreign keys, changing it requires cascading updates to every referencing table — an operation that may be slow, dangerous under concurrent access, or impossible if the database lacks ON UPDATE CASCADE.

      Synthetic keys (surrogate keys)

      A synthetic key (surrogate key) is an identifier generated by the database system at insertion time. It has no meaning outside the database and is never exposed to end users.

      Common implementations:

      MethodStorageProperties
      SERIAL / AUTO_INCREMENT4 or 8 bytesSequential, predictable, fast for B-tree insertion
      UUID (v4)16 bytesRandom, globally unique, no coordination needed across distributed systems
      UUID (v7)16 bytesTime-ordered prefix, good for index locality

      The synthetic key approach has clear advantages:

      • Stable: it never changes once assigned.
      • Guaranteed unique: the DBMS enforces uniqueness.
      • Under your control: you define the generation policy, not an external agency.
      • Simple: often just an integer, making foreign keys compact and joins fast.

      The only safe approach for large, evolving systems is to use synthetic keys for all primary keys. This is not dogmatic but practical: decades of operational experience show that natural keys eventually cause problems, and the cost of fixing them is far higher than the small upfront cost of adding a surrogate key.

      Natural keys still need constraints

      Using a synthetic primary key does not mean you abandon natural key constraints. A table of employees might have:

      employee_id  SERIAL PRIMARY KEY,    -- synthetic
      ni_number    TEXT UNIQUE NOT NULL   -- natural, still enforced

      The UNIQUE constraint on ni_number ensures that business logic consistency is maintained — no two employees can share an NI number — but ni_number is not the primary key. If an NI number changes, only one column in one row needs updating, rather than every foreign key reference across the entire database.

      Formal definition (relational model preview)

      Let R(X)R(X) be a relational schema, where XX is the set of attributes of RR. Let ZXZ \subseteq X be a subset of attributes.

      ZZ is a superkey for RR if, for any records uu and vv in any valid instance of RR:

      u[Z]=v[Z]    u[X]=v[X]u[Z] = v[Z] \implies u[X] = v[X]

      In other words: if two records agree on all attributes in ZZ, they must be the same record (agree on all attributes in XX).

      ZZ is a key for RR if:

      1. ZZ is a superkey for RR, and
      2. No proper subset of ZZ is a superkey for RR.

      The second condition is minimality: you cannot drop any attribute from the key and still have a superkey. Every relation has at least one key (in the worst case, the set of all attributes XX is a superkey, and if no proper subset is a superkey, then XX itself is a key). One key is typically designated as the primary key for implementation purposes.

      Summary

      • A key guarantees that each entity instance can be uniquely identified.
      • Natural keys are appealing but fragile: they are mutable, may not be universal, and lie outside the database’s control.
      • Synthetic keys are stable, guaranteed unique, and under your control — the safe default for any non-trivial system.
      • Natural keys should still carry UNIQUE constraints to enforce business rules.
      • Formally, a superkey is a set of attributes that functionally determines all attributes; a key is a minimal superkey.
    • Relationships and Cardinality

      Relationships

      Relationships represent the verbs of our domain — the associations between entities. In an ER diagram, relationships are drawn as diamonds connected by lines to the participating entity rectangles.

      A relationship type defines a structure; relationship instances are the concrete pairings:

      Martin Campbell directed GoldenEye. Guy Ritchie directed Sherlock Holmes. John McTiernan directed Die Hard.

      The same relationship type (Directed) has many instances, each linking a Person entity to a Movie entity.

      Cardinality: many-to-many

      The cardinality of a relationship describes how many entities on each side can participate. Consider the relationship Directed between Person and Movie:

      • One person can direct multiple movies.
      • One movie can have multiple directors (e.g., the Wachowskis co-directing The Matrix).

      This is a many-to-many relationship: any Person can be related to zero or more Movies, and any Movie can be related to zero or more Persons.

      In ER diagram notation, a many-to-many relationship has no arrows — just plain lines connecting the relationship diamond to the entities:

      Many-to-Many Relationship: a Person can direct multiple Movies, and a Movie can have multiple Directors

      Recursive relationships

      A relationship can connect an entity set to itself. This is a recursive (or self-referencing) relationship. For example, is_sibling relates a Person to another Person:

      Recursive Relationship: is_sibling relates a Person entity back to another Person entity

      is_sibling is symmetric: if Alice is a sibling of Bob, then Bob is a sibling of Alice. Not all recursive relationships are symmetric — is_manager_of on an Employee entity is recursive but not symmetric.

      Cardinality annotations

      There are numerous notations for recording cardinality on ER diagrams: arrowheads, crow’s foot notation, UML multiplicities, (min, max) pairs, and more. The detailed syntax of these notations is not examinable in the Part IA course. What matters is understanding the concept of cardinality and being able to read and reason about the nature of a relationship:

      Relationship typeDescription
      Many-to-manyEach entity on either side can be related to any number of entities on the other side (including zero).
      Many-to-oneEach entity on the “many” side relates to at most one entity on the “one” side.
      One-to-manyThe inverse of many-to-one.
      One-to-oneEach entity on each side relates to at most one entity on the other side.

      The important skill is not the precise diagramming convention but the ability to look at a relationship and determine its cardinality from the real-world rules of the domain.

      Participation constraints

      Beyond cardinality (how many), there is the question of participation: must an entity participate, or can it exist independently?

      • Total participation (double line): every entity in the set must participate in the relationship. Example: every Module must be offered by some Department.
      • Partial participation (single line): an entity may exist without participating. Example: not every Person directs a Movie.

      Participation is distinct from cardinality. A relationship can be many-to-one with total participation on the “many” side, partial on the “one” side, or any combination.

      Summary

      • Relationships (diamonds) connect entities and represent associations in the domain.
      • Many-to-many relationships have no arrows; both sides can be associated with any number of entities on the other side.
      • Recursive relationships connect an entity set to itself and may or may not be symmetric.
      • Cardinality notation details are not examinable, but understanding the conceptual types (many-to-many, many-to-one, one-to-one) is essential.
      • Participation constraints specify whether involvement in a relationship is mandatory or optional.
    • Ternary Relationships and Relationship Attributes

      Relationship attributes

      Relationships can carry attributes of their own — properties that belong to the association, not to either participating entity. In an ER diagram, relationship attributes are drawn as ovals connected to the relationship diamond.

      Consider the Acted_In relationship between Person and Movie:

      Relationship Attribute: the 'role' attribute belongs to the Acted_In relationship, not directly to Person or Movie

      The attribute role describes the character an actor played — it belongs to the Acted_In relationship, not to the Person (a person can play many roles) nor to the Movie (a movie has many roles).

      The modelling problem

      A binary relationship with a simple attribute like role works well when an actor plays a single role in a given film. But what about Jennifer Lawrence playing both Raven and Mystique in X-Men: First Class?

      The straightforward solution — storing multiple roles as a comma-separated string — is a multi-valued attribute. This violates the First Normal Form (1NF) rule of atomicity: each attribute value should be a single, indivisible value. Text processing at that level (parsing comma-separated lists in SQL) is an unspeakable data modelling sin. Queries become fragile, indexing is impossible, and aggregate operations collapse.

      Ternary relationships

      One solution is to promote role to a full entity and make Acted_In a ternary relationship:

      Ternary Relationship: Acted_In links three participating entities: Person, Movie, and Role

      This cleanly models the situation: Jennifer Lawrence has two distinct Acted_In instances — one linking her to X-Men: First Class via the role “Raven”, another via the role “Mystique”.

      But this raises a philosophical question: is a “role” a real-world entity in its own right? The answer depends on the application. A casting agency might reasonably model Role as an independent entity with its own attributes (character name, age range, gender, whether it requires a specific accent). A simple film database might prefer to keep the model lighter.

      Decomposition into binary relationships

      An alternative is to decompose the ternary relationship into binary relationships using an artificial entity (sometimes called a reifying entity or an associative entity):

      Decomposition with Artificial Entity: using an associative 'Casting' entity to decompose the ternary relationship into binary relationships

      Casting is an artificial entity created purely to hold the relationship data. It has a synthetic key and carries the role attribute. This approach avoids ternary relationships altogether, at the cost of introducing an extra entity.

      The textbook (Lemahieu 3.2.6) discusses a risk with this decomposition: data loss. When you decompose an nn-ary relationship into binary relationships, you may lose the ability to enforce the constraint that a particular combination of entities must be unique. Two binary relationships can independently record the same pairings without detecting a duplicate ternary instance.

      Attribute or entity?

      Whether to model a concept as an attribute or as an entity connected by a new relationship depends on the scope of the data model. Consider ReleaseDate:

      • If every movie has at most one release date (e.g., a local cinema database), a simple attribute on Movie works well.
      • If the scope is global, movies have different release dates in different countries (UK: 24 November, US: 22 November, Japan: 15 December). In that case, ReleaseDate might need to be an entity related to both Movie and Country:

      Attribute or Entity: representing ReleaseDate as a relationship with a 'date' attribute between Movie and Country

      The guiding principle: when a property can have multiple values per entity, or when it has meaningful relationships of its own, it should be modelled as an entity. When it is a simple, single-valued fact about an entity in the current scope, an attribute suffices.

      Summary

      • Relationships can carry attributes that belong to the association, not to either participant.
      • Multi-valued attributes violate 1NF and lead to fragile, unindexable queries.
      • Ternary relationships (or decomposed binary relationships with artificial entities) solve the multi-valued attribute problem.
      • Decomposing an nn-ary relationship into binary relationships may introduce data loss — the inability to enforce uniqueness of combined entity instances.
      • The choice between “attribute” and “entity” depends on scope: single-valued and simple is an attribute; multi-valued or independently meaningful is an entity.
    • Many-to-One, One-to-Many, and One-to-One

      Many-to-one relationships

      A many-to-one relationship is shown with an arrow in the ER diagram. The arrow points from the “many” side to the “one” side: each entity on the “many” side is related to at most one entity on the “one” side.

      Consider the relationship Works_In between Employee and Department:

      Many-to-One Relationship: an Employee works in at most one Department, but a Department contains multiple Employees

      The arrow points to Department, meaning every Employee is related to at most one Department. A Department, however, can have many Employees — the relationship is many-to-one.

      Formally: a relationship RR is many-to-one between TT (the source) and SS (the target) when an arrow points from TT to SS. For each entity tTt \in T, there is at most one entity sSs \in S such that (t,s)R(t, s) \in R.

      One-to-many relationships

      One-to-many is simply the inverse of many-to-one. If Works_In is many-to-one from Employee to Department, then it is one-to-many from Department to Employee. No new concept is needed — the arrow just points in the opposite direction.

      In practice, you usually orient the arrow to match the natural reading: “an Employee works in one Department” (arrow to Department), “a Department contains many Employees” (no arrow from Department).

      One-to-one relationships

      If a relationship is both many-to-one between SS and TT and many-to-one between TT and SS (that is, two arrows, one on each side), then it is one-to-one:

      One-to-One Relationship: each Person has at most one Passport, and each Passport belongs to at most one Person

      Each Person has at most one Passport; each Passport belongs to at most one Person.

      One-to-one relationships seldom occur in well-designed databases because they often suggest that the two entities could reasonably be merged into a single table. Why separate Person and Passport if every Person has exactly one Passport and vice versa? A legitimate reason might be:

      • Access control: passport data is more sensitive and stored in a separately encrypted table.
      • Optionality: not every Person has a Passport, and storing many NULL columns is wasteful.
      • Modularity: the Passport table is managed by a different subsystem or microservice.

      What “one-to-one” does NOT mean

      A common confusion: “one-to-one cardinality” does not mean a 1-to-1 correspondence in the mathematical sense (a bijection). It does not require that every entity on one side must correspond to exactly one entity on the other side. Participation is separate from cardinality.

      Consider this valid database instance under the Has relationship (one-to-one between Person and Passport):

      PersonPassport
      AliceP-001
      BobP-002
      Carol— (no passport)
      — (no person)P-003 (cancelled passport, unassigned)

      The constraint enforced by “one-to-one” is: no Person maps to more than one Passport, and no Passport maps to more than one Person. It does not enforce that every Person has a Passport, nor that every Passport has a Person.

      A violation of one-to-one cardinality would be:

      PersonPassport
      AliceP-001
      AliceP-002

      Alice has two passports — this breaks the rule that each Person relates to at most one Passport.

      Formal notation

      There are many competing diagrammatic notations for cardinality. The Part IA course does not require memorising any particular convention in detail. Here is a brief survey for awareness:

      NotationMany-to-manyMany-to-oneOne-to-one
      Arrow styleNo arrowsArrow to “one” sideArrows on both sides
      Crow’s foot──0< or `──<``──
      UML*..*1..* on “many” side1..1
      (min, max)(0, N)──(0, M)(0, N)──(1, 1)(1, 1)──(1, 1)

      The detailed syntax of crow’s foot, UML multiplicities, and (min, max) notation is not examinable. What matters is the ability to read a diagram, identify the nature of the relationship, and reason about whether a given database instance satisfies the cardinality constraints.

      Summary

      • Many-to-one: an arrow points from the “many” side to the “one” side. Each source entity relates to at most one target entity.
      • One-to-many: the inverse of many-to-one.
      • One-to-one: arrows on both sides. Rare in practice; often signals entities that could be merged.
      • One-to-one cardinality does not imply a bijection — participation is separate.
      • Diagrammatic notations vary, but the conceptual understanding of cardinality is what matters for schema design.
    • Weak Entities and Discriminators

      Definition

      A weak entity is an entity whose existence depends on the existence of another entity, called its identifying entity (or owner entity). A weak entity cannot be uniquely identified by its own attributes alone; it requires the identifying entity’s key as part of its identification.

      In ER diagrams:

      • Weak entities are drawn as double-bordered rectangles.
      • The identifying relationship is drawn as a double-bordered diamond.
      • The weak entity’s partial key (discriminator) is underlined with a dashed line.

      Example: AlternativeTitle

      Consider a film database. A Movie has a unique movie_id. The same movie might be released under different titles in different territories:

      Movie (identifying)AlternativeTitle (weak)
      The Avengers (2012)“Avengers Assemble” (UK)
      The Avengers (2012)“Marvel’s The Avengers” (US marketing)
      Harry Potter and the Philosopher’s Stone”Harry Potter and the Sorcerer’s Stone” (US)

      An AlternativeTitle does not exist independently; it exists only in relation to a specific Movie. If the Movie is deleted, all its alternative titles should be deleted too.

      The attribute alt_id (e.g., 1, 2, 3 within each movie) is a discriminator, not a key. To uniquely identify an AlternativeTitle, you need both:

      (movie_id,alt_id)(\text{movie\_id}, \text{alt\_id})

      Neither attribute alone suffices:

      • alt_id = 1 could refer to the first alternative title of any movie.
      • movie_id = tt0848228 tells you which movie, but not which of its several alternative titles.

      The discriminator is unique only within the scope of a single identifying entity. The ER diagram looks like:

      Weak Entity Diagram: Movie strong entity identifies AlternativeTitle weak entity (double bordered) via HasAltTitle identifying relationship (double diamond)

      The double border on AlternativeTitle and the double diamond on HasAltTitle signal the weak entity pattern.

      Relational implementation

      When a weak entity is mapped to a relational schema, its table includes the identifying entity’s primary key as part of its own composite primary key, plus a foreign key constraint:

      CREATE TABLE AlternativeTitle (
          movie_id    INTEGER NOT NULL,
          alt_id      INTEGER NOT NULL,
          alt_title   TEXT NOT NULL,
          territory   TEXT,
          PRIMARY KEY (movie_id, alt_id),
          FOREIGN KEY (movie_id) REFERENCES Movie(movie_id)
              ON DELETE CASCADE
      );

      The composite primary key (movie_id, alt_id) enforces uniqueness of alternative titles within each movie. The ON DELETE CASCADE clause reflects the existential dependency: when the parent Movie is deleted, all its AlternativeTitles are automatically removed.

      Identifying vs. non-identifying relationships

      Not every foreign key relationship implies a weak entity. A non-identifying relationship is one where the child entity has its own independent primary key and the foreign key is just an attribute:

      CREATE TABLE Employee (
          employee_id  SERIAL PRIMARY KEY,
          name         TEXT NOT NULL,
          dept_id      INTEGER REFERENCES Department(dept_id)  -- non-identifying
      );

      Here, Employee is a strong entity: it is identified by employee_id alone. The dept_id foreign key merely records which department the employee belongs to, but Employee could exist even if Department were deleted (assuming nullable foreign key or ON DELETE SET NULL).

      When to use weak entities

      Weak entities are appropriate when:

      1. Existential dependency: the entity cannot meaningfully exist without the owner.
      2. Identification dependency: the entity’s own attributes are insufficient for a global unique key.
      3. Cascade semantics: deletion of the owner should logically delete the owned entities.

      Common examples:

      DomainStrong entityWeak entityDiscriminator
      FilmMovieAlternativeTitlealt_id
      E-commerceOrderLineItemline_number
      UniversityModuleExamPaperpaper_number
      HealthcarePatientAppointmentvisit_date

      In each case, the weak entity makes no sense without its owner — a LineItem without an Order is meaningless, and its line_number = 1 is only meaningful within the scope of a specific order.

      Summary

      • A weak entity depends on an identifying (owner) entity for its existence and identification.
      • Drawn with double borders; the identifying relationship uses a double diamond.
      • A discriminator (partial key) is unique only within the scope of one owner.
      • In SQL, the weak entity’s table uses a composite primary key (owner’s key + discriminator) with a foreign key constraint.
      • Weak entities differ from ordinary foreign-key relationships: the owner’s key is part of the weak entity’s own identity, not just an attribute.
    • Entity Hierarchies (IsA)

      The IsA relationship

      An entity hierarchy (also called an IsA hierarchy, generalisation/specialisation, or superclass/subclass) models the situation where one entity type is a refinement of another. The sub-entity (child) is a specific kind of the super-entity (parent).

      Consider a university database:

      Entity Hierarchy Diagram: Employee superclass generalizing into Temporary_Employee and Contract_Employee subclasses via an IsA triangle

      Temporary_Employee and Contract_Employee are both kinds of Employee. They inherit all attributes (including the key employee_id) and all relationships of the parent entity. The sub-entities add attributes specific to their subtype (hourly_rate for temporary staff, contract_id for contract staff).

      Key properties

      There are several important properties of the IsA hierarchy:

      1. Attribute inheritance: sub-entities automatically possess all attributes of their parent. There is no need to repeat employee_id or name in the sub-entity definitions.
      2. Relationship inheritance: any relationship the parent participates in is also inherited by the sub-entities. If Employee has a Works_In relationship with Department, then both Temporary_Employee and Contract_Employee also have that relationship.
      3. No separate key: sub-entities do not have their own key or discriminator. They are identified by the parent’s key. A Temporary_Employee with employee_id = 142 is the same real-world entity as the Employee with employee_id = 142.
      4. Multiple inheritance: an entity can have multiple parent types (though this is rare in practice and some modelling disciplines discourage it).

      Coverage constraints

      An IsA hierarchy may specify whether the sub-entities cover all possible instances of the parent:

      ConstraintMeaning
      Total (double line from parent to IsA)Every Employee must be either a Temporary_Employee or a Contract_Employee. No plain Employee instances exist.
      Partial (single line)There may be Employee instances that are neither temporary nor contract staff (e.g., permanent salaried employees).

      There may also be a disjointness constraint:

      ConstraintMeaning
      DisjointAn Employee cannot be both a Temporary_Employee and a Contract_Employee simultaneously.
      OverlappingAn Employee could belong to multiple sub-entities (e.g., someone who is both a Lecturer and a Researcher).

      Relational mapping strategies

      There are three standard approaches for mapping an entity hierarchy to relational tables. Choosing among them involves trade-offs between query complexity, storage efficiency, and constraint enforcement.

      Strategy 1: Single table with type tag

      Put all attributes — parent and all sub-entities — into one table, with a discriminator column and nullable sub-entity columns:

      CREATE TABLE Employee (
          employee_id  SERIAL PRIMARY KEY,
          name         TEXT NOT NULL,
          emp_type     TEXT NOT NULL CHECK (emp_type IN ('temp', 'contract')),
          hourly_rate  NUMERIC,       -- NULL for contract employees
          contract_id  TEXT           -- NULL for temporary employees
      );
      Trade-offDetail
      ProSimple queries; no joins needed to get full employee data.
      ConMany NULL columns; wasted space. Cannot enforce NOT NULL on sub-entity attributes (a Temporary_Employee must have an hourly_rate, but the column must allow NULLs for contract rows).
      ConAdding a new sub-entity requires altering the table schema.

      Strategy 2: Separate tables for sub-entities

      One table for the parent, and one table for each sub-entity containing only the sub-entity-specific columns, with the parent’s key as both primary key and foreign key:

      CREATE TABLE Employee (
          employee_id  SERIAL PRIMARY KEY,
          name         TEXT NOT NULL
      );
      
      CREATE TABLE Temporary_Employee (
          employee_id  INTEGER PRIMARY KEY REFERENCES Employee(employee_id),
          hourly_rate  NUMERIC NOT NULL
      );
      
      CREATE TABLE Contract_Employee (
          employee_id  INTEGER PRIMARY KEY REFERENCES Employee(employee_id),
          contract_id  TEXT NOT NULL
      );
      Trade-offDetail
      ProNo nullable columns; sub-entity-specific NOT NULL constraints are enforceable.
      ConQuerying all attributes of a Temporary_Employee requires a join with the parent table.
      ConDoes not enforce disjointness natively — a single employee_id could appear in both Temporary_Employee and Contract_Employee unless additional constraints are added.

      Strategy 3: One table per concrete sub-entity

      Separate tables for each concrete sub-entity, each containing all inherited attributes plus its own specific attributes. No table for the abstract parent:

      CREATE TABLE Temporary_Employee (
          employee_id  SERIAL PRIMARY KEY,
          name         TEXT NOT NULL,
          hourly_rate  NUMERIC NOT NULL
      );
      
      CREATE TABLE Contract_Employee (
          employee_id  SERIAL PRIMARY KEY,
          name         TEXT NOT NULL,
          contract_id  TEXT NOT NULL
      );
      Trade-offDetail
      ProNo joins needed for any query; all attributes in one place.
      ConIf a relationship points to Employee (the parent), you cannot declare a foreign key to a single table — you must use a view, a union, or application-level logic.
      ConKeys are no longer globally unique across the hierarchy. Two sub-entity tables could independently generate employee_id = 1.

      Multi-level hierarchies

      IsA hierarchies can extend to multiple levels. For example:

      Employee
        └── Academic_Staff
              ├── Lecturer
              └── Professor
        └── Administrative_Staff
              ├── Registrar
              └── Technician

      The same mapping strategies apply recursively. A multi-level hierarchy mapped with Strategy 2 would require joins through several tables to reconstruct a complete entity; the performance cost must be weighed against the schema clarity.

      Summary

      • An IsA hierarchy models generalisation/specialisation: sub-entities inherit attributes and relationships from the parent.
      • Sub-entities have no independent key; they are identified by the parent’s key.
      • Coverage constraints (total vs. partial) and disjointness constraints (disjoint vs. overlapping) refine the hierarchy.
      • Three mapping strategies exist: single table with type tags, separate sub-entity tables, or one table per concrete sub-entity.
      • The choice involves trade-offs: query complexity, NULL handling, constraint enforcement, and key uniqueness.
  • The Relational Model

    Mathematical relations and Cartesian products, n-ary database relations, the Relational Algebra (selection, projection, union, intersection, difference, product, renaming), and the natural join as the central combination operator

    • Mathematical Relations

      Codd’s radical idea

      In the 1970s, Edgar Codd proposed a simple but powerful idea: give users a model of data and a language for manipulating that data which is completely independent of the details of its representation and implementation. This model is based on mathematical relations.

      By separating the logical view of data from the physical storage, Codd’s relational model decouples development of the DBMS from the development of database applications. Users query the database in terms of the relations they see; the DBMS decides how to execute those queries on whatever storage structures it uses internally.

      Cartesian products

      Let SS and TT be sets. The Cartesian product of SS and TT is:

      S×T={(s,t)sS,tT}S \times T = \{(s, t) \mid s \in S, t \in T\}

      That is, the set of all ordered pairs where the first element comes from SS and the second from TT.

      Example.

      {A,B}×{3,4,5}={(A,3),(A,4),(A,5),(B,3),(B,4),(B,5)}\{A, B\} \times \{3, 4, 5\} = \{(A, 3), (A, 4), (A, 5), (B, 3), (B, 4), (B, 5)\}

      Six pairs total, since S×T=S×T=2×3=6|S \times T| = |S| \times |T| = 2 \times 3 = 6.

      Binary relations

      A binary relation over S×TS \times T is any set RR with:

      RS×TR \subseteq S \times T

      That is, a relation is simply a subset of the Cartesian product. In the example above, one possible relation is:

      R={(A,3),(B,4),(B,5)}R = \{(A, 3), (B, 4), (B, 5)\}

      This particular RR contains three of the six possible pairs. There is no requirement that every element of SS or TT appears in RR, nor that any particular pairing pattern holds.

      Domains and database relations

      In database parlance, the sets SS and TT are referred to as domains. A domain is the set of all possible values that a given column may take (the type of the column).

      We are interested in finite relations RR that are explicitly stored in the database. This distinguishes database relations from mathematical relations studied in logic, such as those arising from integer linear programming constraints, where relations may be infinite or defined by a predicate rather than explicitly enumerated.

      Why finite relations?

      A database stores a finite number of rows. The relational model is grounded in set theory, but the sets we work with are finite and explicit — we can inspect every tuple, insert and delete tuples, and compute results by applying operations to the tuples themselves. The theory of relational algebra (covered in later notes) relies on relations being sets of known tuples.

      Summary

      • Codd’s relational model separates the logical model of data from physical implementation.
      • A Cartesian product S×TS \times T is the set of all ordered pairs from SS and TT.
      • A binary relation over S×TS \times T is any subset RS×TR \subseteq S \times T.
      • In databases, SS and TT are called domains; relations are finite and explicitly stored.
    • n-ary Relations and Named Columns

      From binary to n-ary

      A binary relation involves two domains. Database tables rarely have only two columns. The generalisation is straightforward: if we have nn sets (domains) S1,S2,,SnS_1, S_2, \ldots, S_n, then an n-ary relation RR is:

      RS1×S2××Sn={(s1,s2,,sn)siSi}R \subseteq S_1 \times S_2 \times \cdots \times S_n = \{(s_1, s_2, \ldots, s_n) \mid s_i \in S_i\}

      Each element of RR is an n-tuple, a sequence of nn values drawn from the respective domains.

      Tabular presentation

      N-ary relations are naturally displayed as tables:

      namesidage
      Aliceabc12320
      Bobdef45621
      Carolghi78920

      Here n=3n = 3. Each row is a tuple (Alice, abc123, 20). The three columns correspond to the three domains: string, string, integer.

      The problem with positional columns

      In the mathematical definition, columns are identified by position: the first column corresponds to S1S_1, the second to S2S_2, and so on. In a query language, writing “select column 3” is error-prone and unreadable.

      Named columns and attribute names

      Database relations use named columns: we associate a name AiA_i (called an attribute name) with each domain SiS_i.

      Instead of tuples (s1,s2,,sn)(s_1, s_2, \ldots, s_n), we use records: sets of pairs each associating an attribute name AiA_i with a value in domain SiS_i. A record is:

      {(A1,s1),(A2,s2),,(An,sn)}\{(A_1, s_1), (A_2, s_2), \ldots, (A_n, s_n)\}

      where siSis_i \in S_i.

      Column order does not matter

      In a mathematical tuple, the order of components matters: (A,3)(3,A)(A, 3) \neq (3, A). In a record with named attributes, the order of the pairs does not matter:

      {(name,Alice),(age,20)}={(age,20),(name,Alice)}\{(\text{name}, \text{Alice}), (\text{age}, 20)\} = \{(\text{age}, 20), (\text{name}, \text{Alice})\}

      This matches how SQL works: SELECT name, age FROM Students and SELECT age, name FROM Students both produce valid results; the column order in the output is determined by the SELECT clause, not by any underlying ordering.

      Schema notation

      A database relation RR is therefore defined as a finite set:

      R{{(A1,s1),(A2,s2),,(An,sn)}siSi}R \subseteq \{\{(A_1, s_1), (A_2, s_2), \ldots, (A_n, s_n)\} \mid s_i \in S_i\}

      The schema of RR is the set of attribute-name/domain pairs. Schema notation:

      R(A1:S1,  A2:S2,  ,  An:Sn)R(A_1 : S_1,\; A_2 : S_2,\; \ldots,\; A_n : S_n)

      Example schema.

      Students(name:string,  sid:string,  age:integer)\text{Students}(\text{name} : \text{string},\; \text{sid} : \text{string},\; \text{age} : \text{integer})

      Row order

      Just as column order does not matter, row order does not matter in the relational model. A relation is a set of records. SQL results are unordered unless you explicitly include ORDER BY.

      In practice, rows are stored in some physical order on disk (often a B-tree on the primary key), but the relational model does not expose or rely on that ordering.

      Summary

      • An n-ary relation generalises binary relations to nn domains.
      • Database relations use named columns (attribute names) rather than positional columns.
      • Column order and row order do not matter at the logical level.
      • A schema declares the attribute names and their types.
    • What is a Query Language?

      Definition

      A relational database query language takes a collection of relation instances R1,R2,,RkR_1, R_2, \ldots, R_k as input and returns a single relation instance as output:

      Q(R1,R2,,Rk)Q(R_1, R_2, \ldots, R_k)

      To meet Codd’s goals, the query language should be high-level: the user specifies what data they want, not how to retrieve it. The language is independent of physical data representation — indexes, file structures, and access paths are managed internally by the DBMS.

      Relational Algebra (RA)

      The Relational Algebra is the formal basis for relational query languages. It provides a small set of operations that take relations as input and produce relations as output. Because each operation returns a relation, operations can be composed to form complex queries.

      Abstract syntax

      FormNameMeaning
      Q::=RQ ::= RBase relationThe relation RR itself
      Q::=σp(Q)Q ::= \sigma_p(Q)SelectionRows satisfying predicate pp
      Q::=πX(Q)Q ::= \pi_X(Q)ProjectionColumns in set XX, duplicates removed
      Q::=Q×QQ ::= Q \times QProductConcatenation of every pair of tuples
      Q::=QQQ ::= Q - QDifferenceTuples in first but not second
      Q::=QQQ ::= Q \cup QUnionTuples in either (or both)
      Q::=QQQ ::= Q \cap QIntersectionTuples in both
      Q::=ρM(Q)Q ::= \rho_M(Q)RenamingChange column names per map MM

      Where:

      • pp is a Boolean predicate over attribute values (e.g., A>12A > 12, B=’CS’B = \text{'CS'}).
      • XX is a set of attribute names to keep.
      • MM is a renaming map, e.g., {BB}\{B \mapsto B'\}.

      Well-formedness conditions

      A query QQ must be well-formed: all column names of the result must be distinct. This imposes constraints on certain operators:

      • In Q1×Q2Q_1 \times Q_2, the two sub-queries cannot share any column names (they must be disjoint).
      • In Q1Q2Q_1 \cup Q_2, Q1Q2Q_1 \cap Q_2, and Q1Q2Q_1 - Q_2, the two sub-queries must share all column names (they must be union-compatible).

      If needed, ρ\rho is used to rename columns to satisfy these conditions.

      SQL

      SQL (Structured Query Language) is the dominant relational query language. It originated at IBM in the early 1970s and has been standardised across multiple revisions:

      VersionYearKey features
      SQL-861986First standard
      SQL-891989Minor revision
      SQL-921992Major expansion (outer joins, CHECK constraints)
      SQL:19991999Recursive queries, triggers, OO features
      SQL:20032003Window functions, XML support
      SQL:20082008MERGE, TRUNCATE
      SQL:20112011Temporal databases
      SQL:20162016JSON support
      SQL:20232023Property graph queries, ANY_VALUE

      SQL contains several sub-languages:

      • Query Language (QL): SELECT, FROM, WHERE, GROUP BY, etc.
      • Data Definition Language (DDL): CREATE TABLE, ALTER TABLE, DROP TABLE.
      • Data Manipulation Language (DML): INSERT, UPDATE, DELETE.
      • Data Control Language (DCL): GRANT, REVOKE.
      • System Administration Language: backup, restore, user management.

      Why study RA if we have SQL?

      1. RA’s simple syntax and semantics make it easier to reason about complex queries. Understanding what a query means in RA helps debug SQL.
      2. The RA lends itself to Tripos questions. It provides a compact, unambiguous notation for defining queries — ideal for exam settings.
      3. SQL is vast and continually evolving. RA captures its core relational logic in a handful of operators.

      Summary

      • A query language maps a collection of relation instances to a single relation instance.
      • RA provides the formal mathematical basis: six core operations plus renaming.
      • SQL is the practical language, standardised from SQL-86 through SQL:2023.
      • RA is studied for clarity and exam support; SQL is used in practice.
    • Relational Algebra: Selection, Projection, and Renaming

      Selection (σ\sigma)

      Selection picks the rows of a relation that satisfy a Boolean predicate. It does not change the columns or their names.

      σpredicate(R)\sigma_{\text{predicate}}(R)

      Example. Given a relation Employees(name, department, salary):

      σsalary>50000(Employees)\sigma_{\text{salary} > 50000}(\text{Employees})

      returns all employees earning more than 50,000. The result has the same schema as Employees.

      SQL translation.

      SELECT DISTINCT *
      FROM Employees
      WHERE salary > 50000;

      The asterisk denotes all fields — there is no projection. DISTINCT is needed because SQL defaults to multiset semantics (see Lecture 7), whereas RA operates on sets.

      Predicates can be combined with \land, \lor, ¬\neg:

      σsalary>50000    department=’CS’(Employees)\sigma_{\text{salary} > 50000\;\land\;\text{department} = \text{'CS'}}(\text{Employees})

      SQL:

      SELECT DISTINCT *
      FROM Employees
      WHERE salary > 50000 AND department = 'CS';

      Projection (π\pi)

      Projection picks specified columns and eliminates duplicate rows. It corresponds to the mathematical operation of projecting a relation onto a subset of its attributes.

      πA1,A2,,Ak(R)\pi_{A_1, A_2, \ldots, A_k}(R)

      Example.

      πname,department(Employees)\pi_{\text{name}, \text{department}}(\text{Employees})

      returns all (name, department) pairs in the database, with duplicates removed. If two employees share the same name and department, the projection contains that pair only once.

      SQL translation.

      SELECT DISTINCT name, department
      FROM Employees;

      Note the misleading keyword: SELECT in SQL performs projection, not selection. In RA, projection is π\pi, selection is σ\sigma. The SQL WHERE clause corresponds to RA σ\sigma (selection); the SQL SELECT clause corresponds to RA π\pi (projection).

      A projection with no selection:

      SELECT DISTINCT name, department FROM Employees;
      -- no WHERE clause -- so no RA selection, despite the 'SELECT' keyword

      Renaming (ρ\rho)

      Renaming changes column names. It does not alter data.

      ρ{AC,  BD,}(R)\rho_{\{A \mapsto C,\; B \mapsto D, \ldots\}}(R)

      Example.

      ρ{namefullName,  salaryannualPay}(Employees)\rho_{\{\text{name} \mapsto \text{fullName},\; \text{salary} \mapsto \text{annualPay}\}}(\text{Employees})

      produces a relation with columns fullName, department, annualPay instead of name, department, salary.

      SQL translation.

      SELECT name AS fullName, department, salary AS annualPay
      FROM Employees;

      SQL implements renaming with the AS keyword. Renaming is frequently needed to make schemas compatible for set operations or products.

      Combined use

      RA operators compose naturally. To find the departments of highly-paid employees:

      πdepartment(σsalary>50000(Employees))\pi_{\text{department}}(\sigma_{\text{salary} > 50000}(\text{Employees}))

      SQL:

      SELECT DISTINCT department
      FROM Employees
      WHERE salary > 50000;

      The RA expression reads inside-out: first filter rows, then project columns and deduplicate. SQL reads more like linear prose: decide what columns, from what table, subject to what condition.

      Operator precedence (convention)

      In RA expressions, unary operators (σ\sigma, π\pi, ρ\rho) bind tighter than binary operators (×\times, \cup, -, \cap). When writing queries, use parentheses to make the evaluation order explicit.

      Summary table

      RA operatorSQL equivalentFunction
      σp(R)\sigma_p(R)WHEREFilter rows by predicate pp
      πX(R)\pi_X(R)SELECT DISTINCTPick columns in XX, deduplicate
      ρM(R)\rho_M(R)ASRename columns per map MM
    • Relational Algebra: Union, Intersection, and Difference

      Union-compatible schemas

      For union, intersection, and difference to be defined, the two relations must have union-compatible schemas: the same number of columns, with the same column names and the same (or compatible) domain types.

      Given R(A,B,C)R(A, B, C) and S(A,B,C)S(A, B, C), the set operations are well-formed. If the schemas differ, ρ\rho must be used first to rename columns.

      Union (\cup)

      RSR \cup S returns all tuples that appear in RR or in SS (or both). Since RA operates on sets, duplicates are eliminated.

      RS={ttRtS}R \cup S = \{t \mid t \in R \lor t \in S\}

      SQL.

      (SELECT * FROM R)
      UNION
      (SELECT * FROM S);

      SQL’s UNION eliminates duplicates by default (set semantics). To keep duplicates, use the non-standard UNION ALL.

      Intersection (\cap)

      RSR \cap S returns all tuples that appear in both RR and SS.

      RS={ttRtS}R \cap S = \{t \mid t \in R \land t \in S\}

      SQL.

      (SELECT * FROM R)
      INTERSECT
      (SELECT * FROM S);

      Intersection can be expressed in terms of difference:

      RS=R(RS)=S(SR)R \cap S = R - (R - S) = S - (S - R)

      In practice, SQL’s INTERSECT is clearer and the optimiser will handle the equivalent transformation if needed.

      Difference (-)

      RSR - S returns all tuples that appear in RR but not in SS.

      RS={ttRtS}R - S = \{t \mid t \in R \land t \notin S\}

      SQL.

      (SELECT * FROM R)
      EXCEPT
      (SELECT * FROM S);

      Note: SQL uses the keyword EXCEPT rather than MINUS (some other database systems use MINUS).

      Difference is not commutative: RSSRR - S \neq S - R in general.

      Set semantics vs multiset semantics

      The RA operators work over sets of tuples. A tuple that appears multiple times in a source table appears at most once in the RA result. This is a key distinction:

      ConceptRASQL default
      Duplicates?Never (set)Yes (multiset/bag)
      Effect on cardinalityProjection may reduce rowsProjection keeps all rows
      UNIONEliminates duplicatesEliminates duplicates (but UNION ALL keeps them)

      SQL defaults to multiset (bag) semantics. The DISTINCT keyword forces set semantics. This distinction is covered in detail in Lecture 7. For now, assume that SQL queries in these notes use SELECT DISTINCT where needed to match RA set behaviour.

      Worked example

      Given relations:

      RR:

      AB
      1x
      2y
      3z

      SS:

      AB
      2y
      3z
      4w

      Union RSR \cup S:

      AB
      1x
      2y
      3z
      4w

      Intersection RSR \cap S:

      AB
      2y
      3z

      Difference RSR - S:

      AB
      1x

      Difference SRS - R:

      AB
      4w

      Summary

      • Union, intersection, and difference require union-compatible schemas.
      • All three eliminate duplicates in RA (set semantics).
      • SQL: UNION, INTERSECT, EXCEPT.
      • Difference is non-commutative; intersection can be expressed via difference.
    • Relational Algebra: Product and Renaming

      Product (×\times)

      The RA product is based on the Cartesian product but adapted for database relations. Given R(A1,,Am)R(A_1, \ldots, A_m) and S(B1,,Bn)S(B_1, \ldots, B_n), the product R×SR \times S returns a relation over the combined attributes A1,,Am,B1,,BnA_1, \ldots, A_m, B_1, \ldots, B_n.

      Mathematically, the Cartesian product of two relations (as sets of tuples) would return pairs of tuples:

      {(t,u)tR,uS}\{(t, u) \mid t \in R, u \in S\}

      The RA product instead returns tuples formed by concatenating the fields of each pair:

      {tutR,uS}\{t \cdot u \mid t \in R, u \in S\}

      where \cdot denotes tuple concatenation.

      Example. Suppose:

      RR:

      AB
      1x
      2y

      SS:

      CD
      p10
      q20

      R×SR \times S:

      ABCD
      1xp10
      1xq20
      2yp10
      2yq20

      If RR has mm rows and SS has nn rows, the product has m×nm \times n rows.

      Column name disjointness

      A critical constraint: the schemas of RR and SS must have disjoint column names. If both relations have a column called id, the product would have two columns both called id, which is not a valid relation.

      When column names overlap, renaming is required first. For example, if R(A,B)R(A, B) and S(B,C)S(B, C) share column BB:

      ρ{BB}(S)\rho_{\{B \mapsto B'\}}(S)

      produces S(B,C)S'(B', C), and then:

      R×SR \times S'

      produces a valid relation over (A,B,B,C)(A, B, B', C).

      SQL translations

      There are two equivalent SQL formulations of the product:

      Explicit syntax (preferred).

      SELECT A, B, C, D
      FROM R CROSS JOIN S;

      Implicit syntax (comma-separated tables).

      SELECT A, B, C, D
      FROM R, S;

      Both produce the same result. The CROSS JOIN form makes the intent more explicit.

      The product is rarely used alone

      The product itself is of limited practical use. Its value comes from combining it with selection to form a join. Selecting the product rows where matching columns are equal gives the natural join or equi-join:

      σB=B(R×ρ{BB}(S))\sigma_{B = B'}(R \times \rho_{\{B \mapsto B'\}}(S))

      This pattern is so common that SQL provides a dedicated JOIN ... ON syntax, and RA has the derived \bowtie (natural join) operator (see following note).

      Combining product with renaming

      When writing RA expressions involving product, the standard pattern is:

      1. Rename overlapping columns in one or both sub-queries with ρ\rho.
      2. Apply ×\times to the renamed sub-queries.
      3. Apply σ\sigma to select matching rows.
      4. Apply π\pi to remove duplicate columns.

      Example. Find all pairs of students and lecturers in the same college:

      πname,lname(σStudents.college=Lecturers.college(Students×ρ{namelname,  collegelcollege}(Lecturers)))\pi_{\text{name}, \text{lname}}(\sigma_{\text{Students.college} = \text{Lecturers.college}}(\text{Students} \times \rho_{\{\text{name} \mapsto \text{lname},\; \text{college} \mapsto \text{lcollege}\}}(\text{Lecturers})))

      But this is verbose. The natural join \bowtie (next note) simplifies it.

      Summary

      • R×SR \times S concatenates tuples; the result has R×S|R| \times |S| rows.
      • Column names of RR and SS must be disjoint; use ρ\rho when they overlap.
      • SQL: CROSS JOIN or comma-separated tables in FROM.
      • The product is typically combined with selection to form a join.
    • Natural Join

      Definition

      The natural join (denoted \bowtie) is the RA operator that combines two relations by equating columns with the same name and eliminating duplicate columns from the output.

      Schema notation

      When we write the schema of RR as R(A,B)R(A, B), we mean that the schema is R(AB)R(A \cup B) where AA and BB are disjoint sets of attributes. The notation splits the schema into parts for clarity: AA are the attributes unique to RR, BB are the shared attributes.

      Formal definition

      Given R(A,B)R(A, B) and S(B,C)S(B, C), where BB is the set of attributes shared by both relations, the natural join RSR \bowtie S is a relation over attributes ABCA \cup B \cup C defined as:

      RS={tuR,  vS,  u.[B]=v.[B]t=u.[A]u.[B]v.[C]}R \bowtie S = \{t \mid \exists u \in R,\; v \in S,\; u.[B] = v.[B] \land t = u.[A] \cup u.[B] \cup v.[C]\}

      That is: for every pair of tuples (u,v)(u, v) from RR and SS where the shared attributes match (u.[B]=v.[B]u.[B] = v.[B]), the result tuple tt combines uu‘s unique attributes (u.[A]u.[A]), the shared attributes (u.[B]u.[B], which equals v.[B]v.[B]), and vv‘s unique attributes (v.[C]v.[C]).

      The shared attributes BB appear once in the result, not twice.

      RA expression for natural join

      The natural join can be expressed in terms of primitive RA operators:

      RS=πA,B,C(σB=B(R×ρBB(S)))R \bowtie S = \pi_{A, B, C}\left(\sigma_{B = B'}\left(R \times \rho_{\vec{B} \mapsto \vec{B'}}(S)\right)\right)

      The steps:

      1. Rename the shared columns in SS to distinguish them from those in RR: ρBB(S)\rho_{\vec{B} \mapsto \vec{B'}}(S).
      2. Take the Cartesian product: R×ρBB(S)R \times \rho_{\vec{B} \mapsto \vec{B'}}(S).
      3. Select rows where the shared columns are equal: σB=B()\sigma_{B = B'}(\cdots).
      4. Project to remove the duplicate columns: πA,B,C()\pi_{A, B, C}(\cdots).

      The notation B=BB = B' abbreviates the conjunction of equalities on each attribute in BB: u.B1=v.B1u.Bn=v.Bnu.B_1 = v.B'_1 \land \cdots \land u.B_n = v.B'_n.

      SQL translation

      SQL provides NATURAL JOIN:

      SELECT *
      FROM R
      NATURAL JOIN S;

      This automatically equates all columns with the same name. More commonly, explicit joins are used:

      SELECT *
      FROM R
      INNER JOIN S ON R.B = S.B;

      Worked example

      Students(name, sid, cid) — student name, ID, and the college ID they belong to.

      namesidcid
      Alicea1C1
      Bobb1C2
      Carolc1C1

      Colleges(cid, cname) — college ID and college name.

      cidcname
      C1Churchill
      C2King’s
      C3Trinity

      Students \bowtie Colleges:

      namesidcidcname
      Alicea1C1Churchill
      Bobb1C2King’s
      Carolc1C1Churchill

      The join matches on the shared cid column. Alice and Carol are both matched to Churchill. Bob is matched to King’s. College C3 (Trinity) has no students, so it does not appear in the result. If a student had a cid with no matching college, that student would also be excluded.

      Join variations in SQL

      Beyond the natural join, SQL supports several join types. These go beyond core RA but are essential in practice:

      Join typeSQL syntaxBehaviour
      Inner joinINNER JOIN / JOINRows with matching values in both tables
      Left outer joinLEFT JOINAll rows from left table; NULLs for unmatched right rows
      Right outer joinRIGHT JOINAll rows from right table; NULLs for unmatched left rows
      Full outer joinFULL JOINAll rows from both; NULLs where no match
      Cross joinCROSS JOINCartesian product (no join condition)

      When NULL values are present in the data, the behaviour of joins becomes more nuanced — see the textbook (Lemahieu 7.3.1.5) for details.

      Join conditions beyond equality

      The natural join only handles equality on shared columns. SQL generalises this with arbitrary join predicates:

      SELECT *
      FROM Employees e
      JOIN Departments d ON e.dept_id = d.dept_id AND e.salary > d.budget;

      In RA, a non-equality join must be expressed via product and selection:

      σdept_id=dept_id    salary>budget(R×ρ{}(S))\sigma_{\text{dept\_id} = \text{dept\_id}' \;\land\; \text{salary} > \text{budget}}(R \times \rho_{\{\ldots\}}(S))

      Summary

      • RSR \bowtie S equates shared columns and eliminates duplicates.
      • RA definition: πA,B,C(σB=B(R×ρBB(S)))\pi_{A,B,C}(\sigma_{B=B'}(R \times \rho_{\vec{B} \mapsto \vec{B'}}(S))).
      • SQL: NATURAL JOIN or INNER JOIN ... ON.
      • Outer joins (LEFT, RIGHT, FULL) preserve unmatched rows with NULLs.
      • Non-equality join predicates require product + selection in RA.
  • Mapping ER Models to Relations

    Update anomalies (insertion, deletion, update) from one big table, breaking tables down to reduce redundancy, keys and superkeys with formal definitions, foreign keys and referential integrity, SQL schema definitions with CREATE TABLE, implementing relationships of various cardinalities, and representing weak entities and entity hierarchies in tables

    • One Big Table and Update Anomalies

      The naive approach

      Consider a directed E/R model with two entity sets — Movies and People — connected by a Directed relationship. A naive implementation puts everything into a single table:

      DirectedComplete(movie_id, title, year, person_id, name, birthYear)

      This seems convenient: one query, one table, all the data. But it introduces severe problems.

      Data redundancy

      Each row in DirectedComplete stores a (movie, director) pair. If a director has directed five films, their name and birth year appear in five separate rows. This duplication is the root cause of the three classical update anomalies.

      Insertion anomaly

      An insertion anomaly occurs when we cannot insert a legitimate piece of data without also providing data that we do not yet have — or that is entirely unrelated.

      ScenarioProblem
      Insert a new person who has not yet directed any filmCannot insert: movie_id and title would be NULL, but they are part of the key. Even if we allow NULL, what value should they take?
      Insert a new film that has no directors yetCannot insert: person_id, name, and birthYear would be NULL, but what director do we reference?

      The schema forces us to fabricate nonexistent data or leave it incomplete — both unacceptable.

      Deletion anomaly

      A deletion anomaly occurs when deleting one fact inadvertently destroys another, independent fact.

      Suppose we delete the last film directed by a particular person from the table. We lose not only the fact that the person directed that film, but also the person’s name and birth year — information that we might still need. The person still exists in the real world, but they vanish from our database.

      Update anomaly

      An update anomaly occurs when a single conceptual change requires multiple physical updates — and failing to apply them all leaves the database in an inconsistent state.

      If a director’s name is misspelled in the database, we must correct it in every row where they appear. A transaction that updates only some of those rows (perhaps because of a bug, a partial failure, or concurrent access) creates inconsistent data: the same person appears with two different names in different rows.

      This is not merely a theoretical concern. In a production system, a transaction that must scan and lock many rows to make a conceptually simple update degrades throughput and increases the likelihood of deadlocks.

      The “one fact, one place” principle

      All three anomalies stem from the same root cause: a single fact is stored in multiple places. The fundamental rule of good relational design is:

      Each fact should be stored in exactly one place in the database.

      When facts are duplicated, any update to that fact must be propagated to all copies. The database system cannot do this automatically — it is the schema designer’s responsibility to avoid duplication in the first place.

      Performance implications

      Beyond correctness, data redundancy has direct performance consequences:

      IssueEffect
      Write amplificationUpdating one director’s name requires touching many rows
      Lock contentionA transaction updating many rows holds locks longer, blocking other transactions
      Storage wasteRedundant copies consume disk space and buffer-pool memory
      Cache inefficiencyDuplicated data displaces useful data from the buffer pool

      In a database supporting many concurrent read and write operations, the combination of lock contention and write amplification from redundant schemas can be catastrophic for throughput.

      The remedy

      The solution is to decompose the single big table into smaller tables — one per entity set and one per relationship. This is the subject of the next note.

      Summary

      • A single-table implementation of an E/R model introduces data redundancy.
      • Redundancy causes insertion, deletion, and update anomalies.
      • The principle “one fact, one place” guides good schema design.
      • Redundancy also harms write throughput and increases lock contention.
      • The answer is to break tables down.
    • Breaking Tables Down to Reduce Redundancy

      The decomposition strategy

      To eliminate redundancy, we decompose DirectedComplete into three tables: one for each entity set and one for the relationship.

      Movies(movie_id, title, year)
      People(person_id, name, birthYear)
      Directed(movie_id, person_id)

      Entity tables

      Each E/R entity becomes a relational table. The entity’s attributes become the table’s columns. The entity’s key becomes the table’s primary key.

      EntityTable
      MoviesMovies(movie_id, title, year)
      PeoplePeople(person_id, name, birthYear)

      Each row in Movies stores exactly one movie. Each row in People stores exactly one person. No duplication.

      Relationship tables

      Each E/R relationship also becomes a relational table. For the Directed relationship (a many-to-many relationship between Movies and People), we create:

      Directed(movie_id, person_id)

      The key of Directed is the combination (movie_id, person_id). This is called an “all-key” table because every column is part of the primary key. This pattern is standard for many-to-many relationships: the relationship table has no attributes of its own beyond the foreign keys to the participating entities.

      A concrete example

      Suppose the database contains the following:

      Movies:

      movie_idtitleyear
      m01The Matrix1999
      m02Speed Racer2008
      m03Cloud Atlas2012

      People:

      person_idnamebirthYear
      p01Lana Wachowski1965
      p02Lilly Wachowski1967

      Directed:

      movie_idperson_id
      m01p01
      m01p02
      m02p01
      m02p02
      m03p01
      m03p02
      m03NULL

      Each fact is stored in exactly one place. Lana Wachowski’s name appears only once, in the People table. If we need to correct it, we update one row.

      Reconstructing the original view

      The original DirectedComplete table can be reconstructed using a SQL JOIN:

      SELECT movie_id, title, year, person_id, name, birthYear
      FROM movies
        JOIN directed ON directed.movie_id = movies.movie_id
        JOIN people ON people.person_id = directed.person_id;

      The join combines the three tables on their common key columns, producing exactly the same columns we had in DirectedComplete — but without the redundancy.

      The fundamental trade-off

      Single-table designDecomposed design
      RedundancyHighNone (or minimal)
      Insertion anomaliesPresentAbsent
      Deletion anomaliesPresentAbsent
      Update anomaliesPresentAbsent
      Query simplicitySimple (one table)Requires JOINs
      Write performancePoor (write amplification)Good

      The decomposed design is the “good” design: we accept the cost of writing JOIN queries in exchange for eliminating redundancy and its associated anomalies. JOINs are the price we pay for a well-normalised schema.

      Both entities and relationships are tables

      A key insight: in the relational model, both E/R entities and E/R relationships are implemented as tables. There is no separate construct for relationships. A relationship table is distinguished only by its content — it contains foreign keys referencing entity tables.

      Summary

      • Decompose a big table into one table per entity set and one per relationship.
      • Entity tables store attributes; relationship tables store foreign keys.
      • Many-to-many relationships produce all-key tables.
      • JOINs reconstruct the combined data when needed.
      • The cost of JOINs is the price of eliminating redundancy and anomalies.
    • Keys and Superkeys: Formal Definitions

      Defining a superkey

      Let R(X)R(X) be a relational schema, where XX is the set of attributes (columns) of RR. Let ZXZ \subseteq X be a subset of those attributes.

      ZZ is a superkey for RR if, for any two records uu and vv in any valid instance of RR:

      u[Z]=v[Z]    u[X]=v[X]u[Z] = v[Z] \implies u[X] = v[X]

      In words: if two records agree on all attributes in ZZ, they must be the same record. No two distinct rows in a valid instance of RR can have identical values for all attributes in ZZ.

      The entire set of attributes XX is trivially a superkey for any relation, since u[X]=v[X]u[X] = v[X] tautologically implies u[X]=v[X]u[X] = v[X].

      Defining a key

      ZZ is a key (or candidate key) for RR if:

      1. ZZ is a superkey for RR, and
      2. No proper subset of ZZ is a superkey for RR.

      The second condition is minimality: you cannot remove any attribute from ZZ and still have a superkey. A key is a minimal superkey.

      Notation

      The notation R(Z,Y)R(\underline{Z}, Y) indicates that ZZ is a key for RR, and that RR‘s full set of attributes is ZYZ \cup Y. The underline marks the key.

      For a table with multiple candidate keys, one is designated as the primary key. The others remain alternate keys.

      Example: Movies table

      Schema: Movies(movie_id, title, year)

      • {movie_id}\{\text{movie\_id}\} is a superkey: no two movies share the same movie_id.
      • {movie_id}\{\text{movie\_id}\} is also a key: removing the only element leaves the empty set, which is not a superkey.
      • {movie_id,title}\{\text{movie\_id}, \text{title}\} is a superkey but not a key (it is not minimal: you can drop title).
      • {title,year}\{\text{title}, \text{year}\} might be a key if we assume no two films share both title and year — but this is a design choice, not a formal guarantee.

      Example: Directed table (all-key)

      Schema: Directed(movie_id, person_id)

      • Z1={movie_id}Z_1 = \{\text{movie\_id}\}: not a superkey. A film can have multiple directors, so u[movie_id]=v[movie_id]u[\text{movie\_id}] = v[\text{movie\_id}] does not force u=vu = v.
      • Z2={person_id}Z_2 = \{\text{person\_id}\}: not a superkey. A person can direct multiple films.
      • Z3={movie_id,person_id}Z_3 = \{\text{movie\_id}, \text{person\_id}\}: is a superkey. A given (movie, person) pair appears at most once in the Directed relationship.
      • Z3Z_3 is also a key: neither subset alone is a superkey.
      • No proper subset of Z3Z_3 is a superkey, so Z3Z_3 is minimal. This is an all-key table.

      Example: People table

      Schema: People(person_id, name, birthYear)

      • {person_id}\{\text{person\_id}\} is a key (assuming synthetic keys).
      • {name,birthYear}\{\text{name}, \text{birthYear}\} might be a key if name+birthYear is unique — but it is not safe to rely on.

      Multiple keys

      A relation can have more than one key. A table of UK citizens might have both:

      • {ni_number}\{\text{ni\_number}\} (National Insurance number)
      • {person_id}\{\text{person\_id}\} (synthetic key)

      Both are keys (minimal superkeys), and either could be chosen as primary key. In practice, synthetic keys are preferred (see the note on synthetic keys in Lecture 2).

      Formal property: every relation has a key

      Since the set of all attributes XX is always a superkey, we can always find a key by starting with XX and iteratively removing attributes while the result remains a superkey. The resulting minimal subset is a key. In the worst case, XX itself is the only key (e.g., an all-key relationship table).

      Superkey vs. key summary

      PropertySuperkeyKey
      Uniquely identifies rowsYesYes
      MinimalNot necessarilyYes
      all attributes\text{all attributes} is one?AlwaysOnly if no proper subset is a superkey
      A table can have many?YesYes

      Connection to normalisation

      The concept of a key becomes critical in normalisation (Lecture 5). A relation is in a normalised form if all non-key attributes are functionally dependent on the key — and no other set of attributes. This is a preview: the key is the anchor for all other data in the row.

      Summary

      • A superkey is any set of attributes that uniquely identifies rows.
      • A key is a minimal superkey.
      • {all attributes}\{\text{all attributes}\} is always a superkey.
      • All-key tables occur for many-to-many relationship tables.
      • Every relation has at least one key.
      • The key is the foundation of normalisation: all data should depend on the key, the whole key, and nothing but the key.
    • Foreign Keys and Referential Integrity

      Motivation

      Once we decompose a schema into multiple tables, we need a mechanism to express that a value in one table refers to a value in another. This is the role of the foreign key. Think of a foreign key as a sort of pointer — it does not store the target data, only the identifier that locates it.

      Defining a foreign key

      Let R(Z,Y)R(\underline{Z}, Y) be a relational schema where ZZ is the key of RR. Let S(W)S(W) be another relational schema, and suppose ZWZ \subseteq W — that is, the attributes of ZZ appear as a subset of the attributes of SS.

      We say that ZZ represents a foreign key in SS referencing RR if, for any valid instance of the database:

      πZ(S)πZ(R)\pi_Z(S) \subseteq \pi_Z(R)

      In words: every value of ZZ that appears in table SS must also appear as a key value in table RR. The projection of SS onto ZZ is a subset of the projection of RR onto ZZ.

      Referential integrity

      A database is said to have referential integrity when all foreign key constraints are satisfied. The DBMS enforces these constraints: any INSERT or UPDATE that would cause a foreign key value to reference a nonexistent target is rejected.

      Referential integrity is one of the two fundamental integrity constraints of the relational model. The other is entity integrity: no part of a primary key may be NULL.

      Example: Has_Genre

      Consider the many-to-many relationship between Movies and Genres:

      Has_Genre(movie_id, genre_id)

      This table has two foreign key constraints:

      Foreign keyReferencesMeaning
      movie_idMovies(movie_id)Every movie_id in Has_Genre must exist in Movies
      genre_idGenres(genre_id)Every genre_id in Has_Genre must exist in Genres

      Formally:

      πmovie_id(Has_Genre)πmovie_id(Movies)\pi_{\text{movie\_id}}(\text{Has\_Genre}) \subseteq \pi_{\text{movie\_id}}(\text{Movies}) πgenre_id(Has_Genre)πgenre_id(Genres)\pi_{\text{genre\_id}}(\text{Has\_Genre}) \subseteq \pi_{\text{genre\_id}}(\text{Genres})

      These constraints prevent dangling references. You cannot classify a nonexistent film into a genre. You cannot assign a nonexistent genre to a film.

      Dangling references: a real-world analogy

      A dangling reference is like a GP surgery’s database listing a patient’s doctor as “Dr. Yeti Goosecreature” when no such doctor exists on the staff table. The foreign key constraint catches this: the INSERT into the patient table would fail because doctor_id does not exist in the Doctors table. The system rejects the operation and demands correction.

      Primary key vs. foreign key

      Primary keyForeign key
      PurposeIdentifies a row within its own tableReferences a row in another table
      UniquenessUnique within its tableNot necessarily unique (many rows can reference the same target)
      NullabilityNever NULLMay be NULL (if the relationship is optional)
      ScopeLocal to one tableCross-table, linking two tables
      Formal definitionMinimal superkeyπZ(S)πZ(R)\pi_Z(S) \subseteq \pi_Z(R)

      Foreign keys in all-key tables

      In an all-key relationship table such as Directed(movie_id, person_id), every column is both part of the primary key and a foreign key. The primary key constraint guarantees that no duplicate (movie, person) pairs exist; the foreign key constraints guarantee that both the movie and the person actually exist.

      Multiple foreign keys to the same table

      A table can have multiple foreign keys referencing the same target table. Consider a Transfers table in a banking database:

      Transfers(id, from_account, to_account, amount)

      Both from_account and to_account are foreign keys referencing Accounts(account_id). They play different roles but point to the same table.

      ON DELETE and ON UPDATE actions

      When a referenced row is deleted or its key is updated, the DBMS must decide what happens to the referencing rows. The SQL standard defines several actions:

      ActionBehaviour
      RESTRICT / NO ACTIONReject the deletion/update if any referencing rows exist
      CASCADEDelete/update the referencing rows automatically
      SET NULLSet the foreign key column to NULL in referencing rows
      SET DEFAULTSet the foreign key column to its default value in referencing rows

      The default in most DBMSs is RESTRICT. Choosing the right action depends on the semantics of the relationship.

      Checking referential integrity

      A DBMS checks referential integrity at the end of each statement (or at the end of each transaction, depending on the constraint mode). If a statement would leave the database in a state violating a foreign key constraint, the statement is rolled back and an error is returned to the client.

      Summary

      • A foreign key is a set of attributes in table SS that must match the key of some row in table RR.
      • Referential integrity means all foreign key constraints are satisfied.
      • The DBMS enforces FK constraints by rejecting invalid inserts and updates.
      • All-key tables consist solely of foreign keys that together form the primary key.
      • ON DELETE and ON UPDATE actions control cascading behaviour when referenced rows change.
    • SQL Schema Definitions

      Translating entity tables to SQL

      An entity table from the E/R model becomes a CREATE TABLE statement. The primary key is declared with PRIMARY KEY.

      Simple entity with a synthetic key

      CREATE TABLE genres (
        genre_id INTEGER NOT NULL,
        genre     TEXT    NOT NULL,
        PRIMARY KEY (genre_id)
      );
      • INTEGER / TEXT are SQL types. The course uses varchar(n) for variable-length strings up to nn characters.
      • NOT NULL ensures the column cannot be NULL.
      • PRIMARY KEY (genre_id) declares genre_id as the primary key. This implies both UNIQUE and NOT NULL.

      Entity with multiple attributes

      CREATE TABLE movies (
        movie_id VARCHAR(16)  NOT NULL,
        title    VARCHAR(255) NOT NULL,
        year     INTEGER      NOT NULL,
        PRIMARY KEY (movie_id)
      );
      CREATE TABLE people (
        person_id VARCHAR(16)  NOT NULL,
        name      VARCHAR(255) NOT NULL,
        birthYear INTEGER,
        PRIMARY KEY (person_id)
      );

      Translating relationship tables to SQL

      Many-to-many (all-key) relationship

      CREATE TABLE directed (
        movie_id  VARCHAR(16) NOT NULL REFERENCES movies (movie_id),
        person_id VARCHAR(16) NOT NULL REFERENCES people (person_id),
        PRIMARY KEY (movie_id, person_id)
      );
      • REFERENCES movies (movie_id) declares a foreign key constraint: every movie_id in directed must exist in movies.movie_id.
      • PRIMARY KEY (movie_id, person_id) declares a composite key spanning both columns.
      • Both columns are foreign keys. Together they form the primary key. This is the canonical all-key pattern.

      Relationship with a foreign key to a single-column key

      CREATE TABLE has_genre (
        movie_id VARCHAR(16) NOT NULL REFERENCES movies (movie_id),
        genre_id INTEGER     NOT NULL REFERENCES genres (genre_id),
        PRIMARY KEY (movie_id, genre_id)
      );

      Short form: REFERENCES movies (movie_id) is equivalent to FOREIGN KEY (movie_id) REFERENCES movies (movie_id). The short form can only be used when the foreign key is a single column, and only as a column constraint. The long form is needed for multi-column foreign keys:

      CREATE TABLE some_table (
        col_a INTEGER NOT NULL,
        col_b INTEGER NOT NULL,
        FOREIGN KEY (col_a, col_b) REFERENCES other_table (col_c, col_d)
      );

      Combined example: a small schema

      A schema for a film database with movies, people, genres, directors, and genre assignments:

      CREATE TABLE movies (
        movie_id VARCHAR(16)  NOT NULL,
        title    VARCHAR(255) NOT NULL,
        year     INTEGER      NOT NULL,
        PRIMARY KEY (movie_id)
      );
      
      CREATE TABLE people (
        person_id VARCHAR(16)  NOT NULL,
        name      VARCHAR(255) NOT NULL,
        birthYear INTEGER,
        PRIMARY KEY (person_id)
      );
      
      CREATE TABLE genres (
        genre_id INTEGER NOT NULL,
        genre    TEXT    NOT NULL,
        PRIMARY KEY (genre_id)
      );
      
      CREATE TABLE directed (
        movie_id  VARCHAR(16) NOT NULL REFERENCES movies  (movie_id),
        person_id VARCHAR(16) NOT NULL REFERENCES people  (person_id),
        PRIMARY KEY (movie_id, person_id)
      );
      
      CREATE TABLE has_genre (
        movie_id VARCHAR(16) NOT NULL REFERENCES movies  (movie_id),
        genre_id INTEGER     NOT NULL REFERENCES genres  (genre_id),
        PRIMARY KEY (movie_id, genre_id)
      );

      Naming conventions

      Foreign key column names typically match the primary key column names they reference. This convention makes schemas self-documenting:

      • directed.movie_id references movies.movie_id
      • has_genre.genre_id references genres.genre_id

      When a table has two foreign keys to the same target table, distinguish them by role:

      CREATE TABLE transfers (
        id            INTEGER     NOT NULL PRIMARY KEY,
        from_account  VARCHAR(16) NOT NULL REFERENCES accounts (account_id),
        to_account    VARCHAR(16) NOT NULL REFERENCES accounts (account_id),
        amount        DECIMAL     NOT NULL
      );

      Here from_account and to_account both reference accounts(account_id) but their column names reflect their distinct roles.

      Enforcing constraints

      The DBMS enforces the declared constraints at statement boundaries:

      Attempted operationConstraint violatedResult
      INSERT INTO directed VALUES ('m99', 'p01')FK: m99 not in moviesRejected
      INSERT INTO directed VALUES ('m01', 'p01')PK: duplicate rowRejected
      DELETE FROM movies WHERE movie_id = 'm01'FK: m01 referenced in directedRejected (if RESTRICT) or cascaded (if CASCADE)
      INSERT INTO movies VALUES (NULL, 'Film', 2024)PK: NOT NULL on movie_idRejected

      Overline notation

      In some schema diagrams, foreign keys are indicated visually with overlines. For example, if table WW has attributes A,B,Z,CA, B, Z, C, and ZZ is a foreign key to table RR, we might write W=A.B.Z.CW = \overline{A}.B.\overline{Z}.C, where the overline on ZZ marks it as a foreign key and the overline on AA marks AA as part of the primary key.

      Composite foreign keys

      A foreign key can span multiple columns if the referenced table has a composite key:

      CREATE TABLE enrolment (
        student_id  INTEGER NOT NULL,
        course_id   INTEGER NOT NULL,
        year        INTEGER NOT NULL,
        PRIMARY KEY (student_id, course_id),
        FOREIGN KEY (course_id, year) REFERENCES course_offerings (course_id, year)
      );

      Here (course_id, year) references the composite key of course_offerings.

      Summary

      • CREATE TABLE defines a relation with columns, types, and constraints.
      • PRIMARY KEY declares the key; XX is a minimal superkey.
      • REFERENCES (or FOREIGN KEY ... REFERENCES) declares a foreign key constraint.
      • NOT NULL prevents NULL values.
      • The DBMS enforces all constraints, rejecting invalid operations.
      • Naming conventions make foreign key relationships obvious.
    • Implementing Relationships in Tables

      The clean approach

      Given a relationship RR between entity tables S(Z,W)S(\underline{Z}, W) and T(X,Y)T(\underline{X}, Y), the default approach is to create a separate table:

      R(X,Z,U)R(X, Z, U)

      where UU are any relationship-specific attributes (e.g., a salary paid to an actor for a particular film). Both XX and ZZ serve as foreign keys to TT and SS respectively.

      Many-to-many (M:N)

      For a many-to-many relationship, the separate-table approach is mandatory. The key of RR is (X,Z)(X, Z) — both foreign keys together form the primary key. If the relationship has attributes UU, they are included as non-key columns.

      Example: Directed(movie_id, person_id) with key (movie_id, person_id).

      One-to-many (1:M)

      For a one-to-many relationship, we have two options.

      Option 1: Separate table. Create R(X,Z,U)R(X, Z, U) as above. The key is XX (the “many” side), since each entity on the many side participates in at most one relationship. Requires a JOIN to navigate.

      Option 2: Expand the “many” side table. Expand TT to T(X,Y,Z,U)T(\underline{X}, Y, Z, U), adding the foreign key ZZ and relationship attributes UU directly. Rows that do not participate in the relationship store NULL in ZZ and UU.

      Separate tableExpanded table
      JOINs neededYes, for every navigationNo
      NULL valuesNoneFor rows not in the relationship
      NormalisationCleanViolates normal forms (see below)
      Update complexityInsert/delete into the relationship tableUpdate a single column

      The expansion approach saves a join but introduces NULL columns and violates normalisation principles: ZZ (the foreign key) functionally depends on XX (the key of the expanded table), but ZZ is not itself a key. A later lecture covers normal forms in detail.

      One-to-one (1:1)

      For a one-to-one relationship, either side can absorb the foreign key. Choose the side where NULL values are less common.

      Relationship attributes

      When a relationship carries its own attributes, the clean approach uses a separate relationship table:

      E/R diagram: Actor(id, name) — PerformsIn(character, salary) — Movie(id, title)

      Relational schema:

      Actors(actor_id, name)
      Movies(movie_id, title)
      PerformsIn(actor_id, movie_id, character, salary)

      The character and salary columns belong to the relationship, not to either entity. Placing them in Actors or Movies would be incorrect: an actor can play different characters in different films for different salaries.

      Merging multiple relationships

      Sometimes multiple relationships between the same entity sets are merged into a single table with a type discriminator. The course example is has_position:

      has_position(movie_id, person_id, role, character_name, position_in_credits)

      This single table combines what could have been five separate tables: Directed, ActedIn, Produced, Composed, Wrote. The role column is a type discriminator (e.g., 'director', 'actor', 'producer').

      Advantages:

      • One table instead of five — simpler conceptual model.
      • Queries across all roles require only one table.
      • Adding a new role type requires only a new value in the role column, not a new table.

      Disadvantages:

      • character_name is NULL for non-actor roles; position_in_credits might be NULL for some roles.
      • Redundancy: if we know role = 'director', we can deduce that character_name is NULL. This is a violation of normalisation principles — the value of one non-key column (role) determines whether another column (character_name) must be NULL.
      • Foreign key constraints cannot easily express “an actor must have a character name” at the schema level. This logic moves to application code or triggers.

      The type-tag redundancy problem

      Consider two separate relationship tables:

      R(X, Z, U)     -- e.g., directed roles
      Q(X, Z, V)     -- e.g., acted roles

      Now squash them into one:

      RQ(X, Z, type, U, V)

      where type indicates whether this row belongs to RR or QQ. This introduces a redundancy: the type value determines which of UU or VV is meaningful. If type = 'R', then VV must be NULL. If type = 'Q', then UU must be NULL. The fact that UU is NULL is not independent data — it is computable from type. This violates the principle that data should not be derivable from other data.

      Summary table

      Relationship typeDefault implementationKey of relationship tableAlternative
      M:NSeparate table(X,Z)(X, Z) (composite)None required
      1:MSeparate table or expand the “many” sideXX (the many side)Expand TT: T(X,Y,Z,U)T(\underline{X}, Y, Z, U)
      1:1Foreign key on either sideEither XX or ZZExpand either table
      With attributesSeparate tableAs aboveAttributes placed in the table that makes semantic sense

      Summary

      • The clean approach creates a separate table for each relationship.
      • M:N relationships must use a separate table with a composite key.
      • 1:M relationships can use a separate table or expand the “many” side, trading JOIN cost against normalisation.
      • Merging multiple relationships into one table with a type tag reduces table count but introduces redundancy and weakens constraint enforcement.
      • Relationship attributes belong in the relationship table, not in entity tables.
    • Implementing Weak Entities and Entity Hierarchies

      Weak entities

      A weak entity is one that cannot be uniquely identified by its own attributes alone. It depends on an identifying (owner) entity. In the E/R model, a weak entity is represented by a double-lined rectangle; its identifying relationship is represented by a double-lined diamond.

      Implementation pattern

      Let S(Z,W)S(\underline{Z}, W) be the identifying entity and TT be the weak entity. The weak entity table is:

      T(Z,DISC,Y)T(\underline{Z, \text{DISC}}, Y)

      where:

      • ZZ is a foreign key to SS (the identifying entity).
      • DISC is the discriminator — a partial key that, combined with ZZ, uniquely identifies each instance of TT.
      • YY are the weak entity’s own attributes.

      The primary key of TT is the composite (Z,DISC)(Z, \text{DISC}).

      Example: university modules

      A university offers courses. Each course has modules. A module number like “Databases” is only meaningful within the context of a specific course.

      E/R model:

      • Strong entity: Course(course_id, course_name)
      • Weak entity: Module(number, title, credits), identified by Course via the Includes relationship.

      Relational schema:

      CREATE TABLE courses (
        course_id   INTEGER     NOT NULL,
        course_name VARCHAR(255) NOT NULL,
        PRIMARY KEY (course_id)
      );
      
      CREATE TABLE modules (
        course_id   INTEGER      NOT NULL REFERENCES courses (course_id),
        number      VARCHAR(16)  NOT NULL,
        title       VARCHAR(255) NOT NULL,
        credits     INTEGER      NOT NULL,
        PRIMARY KEY (course_id, number)
      );

      The number discriminator alone is not unique (multiple courses could each have a “module 4”), but (course_id, number) is unique. The foreign key course_id links each module to its identifying course.

      Alternative: expansion into one table

      If the weak entity has a single attribute set (one row per owner-discriminator pair), we can expand the identifying entity:

      R(Z,W,DISC,U,Y)R(\underline{Z}, W, \text{DISC}, U, Y)

      where UU and YY would otherwise have been the attributes of TT. This avoids a join but reintroduces possible NULL values for ZZ values that do not have an associated weak entity instance. The choice between a separate table and expansion follows the same trade-off as 1:1 or 1:M relationships (see the previous note).

      Entity hierarchies

      An entity hierarchy (ISA relationship) occurs when one entity type is a specialisation of another. For example, Employee is a general entity type; Manager and Engineer are specialisations.

      Approach 1: One table per entity (normalised)

      Create a table for each entity in the hierarchy, including the parent:

      Employee(person_id, name, hire_date)
      Manager(person_id, budget)
      Engineer(person_id, specialism)

      The child tables include only the parent’s key and their own specialised attributes. The foreign key constraint πperson_id(Manager)πperson_id(Employee)\pi_{\text{person\_id}}(\text{Manager}) \subseteq \pi_{\text{person\_id}}(\text{Employee}) ensures that every manager is also an employee.

      To retrieve all attributes of a manager, join:

      SELECT e.person_id, e.name, e.hire_date, m.budget
      FROM employee e JOIN manager m ON e.person_id = m.person_id;

      Advantages:

      • No NULL values. Every column is meaningful for every row.
      • Constraints are precise: you cannot accidentally assign a budget to a non-manager.
      • Adding a new sub-type creates a new table without altering existing ones.

      Disadvantages:

      • Requires JOINs to retrieve the full set of attributes for a sub-type entity.
      • Ensuring an entity belongs to at most one sub-type (disjointness) requires application-level logic or triggers.

      Approach 2: Single table with type tags (denormalised)

      Place all attributes of all entities in the hierarchy into a single table with a discriminator column:

      CREATE TABLE employee (
        person_id   INTEGER      NOT NULL PRIMARY KEY,
        name        VARCHAR(255) NOT NULL,
        hire_date   DATE         NOT NULL,
        budget      DECIMAL,        -- NULL for non-managers
        specialism  VARCHAR(255),   -- NULL for non-engineers
        emp_type    VARCHAR(16)  NOT NULL CHECK (emp_type IN ('manager', 'engineer'))
      );

      The emp_type column acts as a type discriminator. Rows where emp_type = 'manager' will have budget populated and specialism set to NULL.

      Advantages:

      • Simple queries: one table, no JOINs.
      • Easy to list all employees regardless of type.

      Disadvantages:

      • Many NULL columns waste space.
      • Schema-level constraints cannot express “managers must have a budget” or “engineers must have a specialism”. These rules move to application code.
      • Adding a new sub-type requires ALTER TABLE to add new columns.
      • The type tag introduces redundancy: specialism IS NULL can be deduced from emp_type = 'manager'.

      Approach 3: One table per concrete sub-type (no parent table)

      Create a table for each concrete sub-type, with all inherited columns duplicated:

      CREATE TABLE manager (
        person_id   INTEGER      NOT NULL PRIMARY KEY,
        name        VARCHAR(255) NOT NULL,
        hire_date   DATE         NOT NULL,
        budget      DECIMAL      NOT NULL
      );
      
      CREATE TABLE engineer (
        person_id   INTEGER      NOT NULL PRIMARY KEY,
        name        VARCHAR(255) NOT NULL,
        hire_date   DATE         NOT NULL,
        specialism  VARCHAR(255) NOT NULL
      );

      There is no Employee parent table. Every manager and engineer row carries all inherited columns.

      Advantages:

      • No JOINs needed for any query.
      • No NULL columns.
      • Each table is self-contained.

      Disadvantages:

      • Duplication of inherited columns.
      • Queries over all employees (regardless of type) require UNION:
      SELECT person_id, name, hire_date FROM manager
      UNION
      SELECT person_id, name, hire_date FROM engineer;
      • No single place to enforce a global constraint across all sub-types (e.g., unique name across managers and engineers).
      • Adding a new sub-type requires a new table with all inherited columns.

      Multi-level hierarchies

      For a hierarchy with more than two levels (e.g., Person → Employee → Manager → SeniorManager), approaches 1 and 3 scale better. Approach 2 (single table) becomes unwieldy as columns proliferate for each level of specialisation.

      Comparison

      CriterionApproach 1 (one per entity)Approach 2 (single table)Approach 3 (one per concrete type)
      NULL valuesNoneManyNone
      JOINs neededYes (for sub-type queries)NoNo (for sub-type queries); UNION for parent queries
      Schema evolutionEasy (add new table)Hard (ALTER TABLE)Medium (new table with all columns)
      Constraint enforcementStrong (FK constraints between tables)Weak (logic in application)Medium (check constraints per table)
      Storage efficiencyGoodPoor (NULL columns)Moderate (duplication of inherited columns)
      Query over all typesUNION across all tablesSimpleUNION across all tables

      Choosing an approach

      • Approach 1 is the default for well-normalised schemas. It is the most flexible and enforces the most constraints.
      • Approach 2 can be acceptable for small, stable hierarchies where query simplicity matters more than storage efficiency.
      • Approach 3 is appropriate when sub-types differ substantially in their attributes and you rarely query across all sub-types.

      In practice, the relational model favours approach 1: it aligns with normalisation principles and keeps the schema maintainable as it evolves.

      Summary

      • Weak entities depend on an identifying entity. Their table includes a foreign key to the owner and a discriminator. The primary key is (owner_key, discriminator).
      • Entity hierarchies can be implemented with one table per entity (normalised), a single table with type tags (denormalised), or one table per concrete sub-type.
      • Approach 1 is the cleanest: no NULLs, strong constraints, easy evolution.
      • Approach 2 is simpler to query but wastes space and weakens constraints.
      • Approach 3 avoids NULLs and JOINs for sub-type queries but requires UNION for parent-type queries and duplicates columns.
  • Transactions and Normalisation

    Transaction processing and the ACID properties, ACID versus BASE in distributed systems, locking and its effect on throughput, redundant data and its consequences, normal forms and normalisation to reduce redundancy, the redundancy-consistency-throughput trade-off, OLAP versus OLTP, and read-oriented versus write-oriented databases

    • Transaction Processing

      What is a transaction?

      A transaction on a database is a series of queries and changes that externally appear to be atomic. To the outside observer, the transaction either happens entirely or not at all.

      Internal and external transactions

      Internal transactions: values are read, perhaps more values conditionally read, and then values are changed based on the values read. All values read or written are inside the database. The DBMS can guarantee atomicity because it controls all the state involved.

      External “transactions”: the idea of an external transaction does not really exist. Some changed values or side effects (like sending an SMS acknowledgement) are external to the DBMS. The DBMS cannot help make these atomic; system designers must think carefully about undoing them. If a transaction that sent an SMS is later aborted, the SMS has already been delivered and cannot be recalled.

      Many DBMS systems allow the application to abort a transaction before it is committed. This is a topic covered in Part IB Concurrent Systems.

      Transaction client flow

      The lifecycle of a transaction from the client’s perspective:

      th = transaction_start()
      
      -- any number of queries and updates, in any order
      -- ...
      
      rc = transaction_commit(th)
      -- or: transaction_abort(th)

      If aborted, all updates made within the transaction are undone by the DBMS. The application programmer does not need to write undo logic for database state.

      In some (optimistic) systems, transaction_commit may itself abort. When this happens, the client must restart the transaction from the beginning.

      Concurrency

      DBMSs support concurrent transactions. Multiple clients can have active transactions simultaneously. The ‘start’ and ‘commit’ calls bracket the body of each transaction. The DBMS is responsible for ensuring that concurrent transactions do not interfere with one another in ways that violate the ACID properties.

      Key points

      AspectDescription
      Internal vs. externalDBMS can only guarantee atomicity for data it controls
      Client APIstart, queries/updates, commit or abort
      Abort semanticsAll database changes are rolled back automatically
      Optimistic systemsCommit itself may fail; client must retry
      ConcurrencyMultiple transactions overlap in time; DBMS arbitrates

      Summary

      • A transaction is an atomic sequence of database operations.
      • The DBMS can guarantee atomicity for internal state, but not for external side effects.
      • The client API follows a start-query-commit/abort pattern.
      • Concurrent transactions are supported; the DBMS manages isolation.
    • ACID Transaction Properties

      The ACID guarantees

      ACID is an acronym for the four properties that a DBMS guarantees for transactions:

      PropertyMeaning
      AtomicityAll changes are performed as a single operation, or none are
      ConsistencyA transaction transforms the database from one consistent state to another
      IsolationConcurrent transactions do not see each other’s intermediate state
      DurabilityOnce committed, changes survive system failures

      Atomicity

      All changes to data are performed as if they are a single operation. All the changes are performed, or none of them are.

      Example: in a funds transfer, if a debit is made from one account, the corresponding credit must be made to the other account. If the system fails after the debit but before the credit, atomicity ensures that the debit is rolled back. Neither half-effect survives.

      Consistency

      Every transaction applied to a consistent database leaves it in a consistent state. The database has integrity constraints (declared in the schema), and a valid transaction must preserve them.

      Example: conservation of money. The total value of funds held over all accounts remains constant. A transfer moves money but does not create or destroy it. If the database starts with pounds1000\\pounds 1000 across all accounts, it still has pounds1000\\pounds 1000 after any valid transaction.

      Isolation

      The intermediate state of a transaction is invisible to other transactions. As a result, transactions that run concurrently appear to be serialised. Each transaction behaves as if it ran alone on the database.

      Example: another concurrent transaction sees the transferred funds in one account or the other, but not in both, nor in neither. It never observes the moment when money has left the source account but not yet arrived at the destination.

      Durability

      After a transaction successfully completes (commits), changes to data persist and are not undone, even in the event of a system failure. The DBMS typically achieves this through write-ahead logging: changes are written to a persistent log before being applied to the database files.

      Implementation note

      Implementing ACID transactions is lectured in Part IB Concurrent and Distributed Systems. The mechanisms (locking, logging, two-phase commit) are not part of this course.

      Summary

      PropertyKey guarantee
      AtomicityAll or nothing
      ConsistencyValid state preserved
      IsolationNon-interference between concurrent transactions
      DurabilitySurvives crashes
    • ACID vs BASE

      BASE: the alternative to ACID

      Many NoSQL systems weaken ACID properties. The result is often called BASE transactions (a deliberate pun on ACID):

      LetterTermMeaning
      BABasically AvailableAvailability is promoted over consistency
      SSoft stateStored values may change without any application intervention owing to eventual consistency updates or network partition
      EEventual consistencyAll readers throughout the system will eventually see the same state as each other

      Exactly what these terms mean varies from system to system. This is an area of ongoing research. BASE is certainly ideal for some applications, but some proponents have lost their faith and fallen back to a relational system.

      The trade-off

      PropertyACIDBASE
      ConsistencyStrong, immediateEventual
      AvailabilityMay block during partitionsPrioritised
      PerformanceLower throughput due to locking overheadHigher throughput
      Partition toleranceSacrificed in AP systemsEmbraced
      Application complexityLowHigh (app must tolerate stale reads)
      Typical useBanking, inventory, financial ledgersSocial media, content delivery, recommendation

      ACID provides strong guarantees but at a performance cost. BASE provides high availability and partition tolerance but sacrifices immediate consistency.

      The CAP theorem

      The CAP theorem (covered in Lecture 1) provides the theoretical underpinning:

      You cannot simultaneously achieve Consistency, Availability, and Partition tolerance in a distributed system.

      Formulated by Eric Brewer in 2000, the theorem states that at most two of the three guarantees can be provided. Since network partitions are inevitable in distributed systems, the practical choice is between:

      • CP (Consistency + Partition tolerance): sacrifice availability. The system may refuse requests during a partition to preserve consistency. Example: traditional RDBMS with synchronous replication.
      • AP (Availability + Partition tolerance): sacrifice consistency. The system continues serving requests even if replicas diverge. Example: DynamoDB, Cassandra.

      The CAP Theorem Triangle: Consistency (C), Availability (A), and Partition Tolerance (P) tradeoff

      A system sits on an edge, never at the centre.

      BASE in practice

      In a BASE system, after an update:

      1. Some replicas receive the update immediately.
      2. Other replicas lag behind, serving stale data.
      3. Over time, all replicas converge to the same state.
      4. The application must be written to cope with stale reads.

      Summary

      • ACID and BASE represent opposite ends of a consistency spectrum.
      • The CAP theorem formalises why you cannot have everything in a distributed system.
      • ACID prioritises consistency; BASE prioritises availability.
      • The choice depends on the application’s tolerance for stale data.
    • Locks and Throughput

      What is a lock?

      A lock is a special software or hardware primitive that provides mutual exclusion. A resource can be locked for exclusive access by one concurrent application, which must unlock it again after use. Other contending applications have to wait, which delays their completion.

      Locks are acquired and released by transactions during execution. The DBMS uses locks internally to enforce the isolation property of ACID. How locks are used to implement ACID is not part of any DBMS API — it is part of the “secret sauce” implemented by each vendor.

      Granularity

      Locks can be placed along a spectrum:

      GranularityWhat is lockedEffect
      Very coarse-grainedThe entire databaseOnly one transaction can run at a time
      Coarse-grainedA whole tableConcurrent access to different tables allowed
      Medium-grainedA page or block of rowsModerately concurrent
      Fine-grainedA single rowHigh concurrency
      Very fine-grainedA single data value (field)Maximum concurrency

      Locking and throughput

      If transactions lock large amounts of data, or lock frequently used data, fewer concurrent updates can be supported. This degrades throughput.

      The relationship between locking and throughput:

      Lock Granularity vs Throughput Tradeoff: concurrency increases with finer locks, but is eventually bottlenecked by lock management CPU/memory overhead

      Finer granularity generally permits higher concurrency, but at the cost of greater overhead in managing many small locks. There is a trade-off: managing thousands of row-level locks consumes more CPU and memory than managing a handful of table-level locks.

      Important observation

      If transactions lock large amounts of data or frequently used data, fewer concurrent updates can be supported. This has direct implications for schema design:

      • Tables with many columns: a transaction updating one column may lock the entire row, blocking updates to other columns.
      • Hot rows: rows that are updated frequently become contention points.
      • Indexes: updating an indexed column requires locking the index structure as well as the row.

      Lock types

      Lock typeAllowsTypical SQL
      Shared (read) lockOther readers, no writersSELECT
      Exclusive (write) lockNeither readers nor writersUPDATE, DELETE, INSERT

      Multiple transactions can hold shared locks on the same resource simultaneously. Only one transaction can hold an exclusive lock, and no shared locks may coexist with it.

      Summary

      • Locks enforce mutual exclusion, enabling isolation in concurrent systems.
      • Granularity ranges from entire-database to single-field locks.
      • Coarser locks reduce concurrency and throughput.
      • Finer locks increase overhead but permit more parallelism.
      • Locking is an internal DBMS mechanism, not exposed in the API.
    • Redundant Data and Update Anomalies

      Definition

      Data in a database is redundant if it can be deleted and then reconstructed from the data remaining in the database.

      Why is redundancy problematic?

      If data is held in more than one place, copies can disagree. When a value is updated in one location but not another, the database enters an inconsistent state.

      In a database supporting a high rate of update transactions, high levels of data redundancy imply that correct transactions may have to acquire many locks to consistently update redundant copies. This reduces throughput.

      When is redundancy useful?

      Redundant data can also be useful. If updates are rare, having multiple copies can:

      • Increase read bandwidth (more replicas to read from)
      • Speed up lookup (a denormalised table avoids joins)
      • Reduce data movement cost (a local copy avoids network round-trips)

      What do we mean by ‘multiple copies’?

      Two components contribute to query cost:

      ComponentDescription
      Lookup costFinding the appropriate records via searching and key matching
      Data movement costSending the query and receiving the result over the network

      Schema examples

      Consider these schemas and their performance characteristics:

      Example 1: duplicate tables

      SchemaStructure
      R0(K, V)One table
      R1(K, V) and R2(K, V)Two identical tables

      Redundancy here might increase read throughput if the DBMS can parallelise reads across the two tables. However, every write must update both copies, doubling the write cost.

      Example 2: split vs. combined

      SchemaStructure
      A0(K, V1, V2)Combined table
      A1(K, V1) and A2(K, V2)Split tables

      Combining reduces lookup costs (one table scan instead of two) but may lock more data per transaction. A query needing only V1 must still lock the entire row in A0, including V2.

      The fundamental tension

      More redundancy  --->  Faster reads
      More redundancy  --->  Slower writes

      Redundancy speeds up reads but slows down writes. The right balance depends on the application’s read-to-write ratio.

      Update anomalies

      An update anomaly occurs when changing a single logical fact requires modifying multiple rows, and a partial update leaves the database inconsistent.

      Example: a GP’s surgery address is stored in every patient record. If the surgery moves, every patient row must be updated. A partial update leaves some patients pointing to the old address.

      Anomaly typeDescription
      Update anomalyChanging data in one place but not in others
      Insertion anomalyCannot insert a fact without also inserting unrelated data
      Deletion anomalyDeleting a fact inadvertently removes other information

      Summary

      • Redundant data can be reconstructed from remaining data.
      • Redundancy causes disagreement between copies and update anomalies.
      • Redundancy can also improve read performance when writes are rare.
      • The fundamental trade-off: redundancy helps reads, hurts writes.
    • Normal Forms and Normalisation

      What is a normalised database?

      A normalised database is one that has little or no redundant data. Typically, redundant relational databases have tables with too many attributes.

      A rule of thumb

      All table data should either be key or semantically depend on the key.

      If you can spot data that does not directly depend on the key (recall the GP’s age field from Lecture 1), that part of the table should be split off into a separate table.

      This procedure is then repeated on the new tables until closure: a fixed-point is reached where no further decompositions are needed.

      Decomposition as division

      “Splitting off” is essentially a division transform: an information-preserving rewrite that can be reversed using a join, which behaves like a multiplication.

      Lossless Decomposition: splitting relation R(A, B, C) into R1(A, B) and R2(A, C), which can be Natural Joined back together without loss of information

      A decomposition is lossless if the original table can be perfectly reconstructed by joining the decomposed tables. Not all decompositions are lossless; a good normalisation produces only lossless decompositions.

      Automated vs. manual normalisation

      Automated procedures have been mooted to convert databases into normal forms:

      Normal formKey property
      First Normal Form (1NF)All attributes are atomic (no repeating groups)
      Second Normal Form (2NF)No partial dependencies on a composite key
      Third Normal Form (3NF)No transitive dependencies
      Boyce-Codd Normal Form (BCNF)Every determinant is a candidate key

      But computers cannot really understand what “semantically depends” means. Doing a good job of Entity-Relationship modelling in the first place, or manual decomposition, is generally preferable.

      Syllabus scope

      For this course, you only need to know that data should ideally be functionally or semantically dependent on the primary key. The subtleties of 3NF vs. BCNF are off the syllabus.

      Closure

      Closure: an iteration is repeated until there are no further changes. A fixed-point is found.

      Normal form conversion is a closure iteration:

      1. Identify an attribute that does not depend on the key.
      2. Split it into a new table along with the key it does depend on.
      3. Repeat from step 1 on all resulting tables.
      4. Stop when every attribute in every table depends on the key of that table.

      Why normalise?

      BenefitExplanation
      Reduced redundancyEach fact stored once
      Higher update throughputFewer locks needed per update
      Easier consistencyNo redundant copies to keep in sync
      Simpler schema evolutionChanges affect fewer places

      Summary

      • A normalised database minimises redundant data.
      • The rule of thumb: every attribute should depend on the key.
      • Normalisation proceeds by repeated decomposition until closure.
      • Decomposition is a lossless division; JOIN is the inverse.
      • Manual modelling is generally preferable to automated normalisation.
    • Redundancy, Consistency, Throughput, OLAP and OLTP

      The redundancy/consistency/throughput trade-off

      The relationship between redundancy, consistency, and throughput can be illustrated along a spectrum:

      Database Workload Tradeoff Spectrum: contrasting normalized OLTP (write-optimized) and denormalized OLAP (read-optimized) database designs

      • Low redundancy: need only lock a few data items per update. Write throughput is high. Consistency is strong.
      • High redundancy: fewer files or blocks need be accessed per query. Read latency is low. Consistency is relaxed.

      Read-oriented databases

      Introducing data redundancy can speed up read-oriented transactions at the expense of slowing down write-oriented transactions.

      Database indexes demonstrate this point clearly. An index is redundant data (it duplicates key values from the base table) that speeds up reads but must be maintained on every write. Each INSERT, UPDATE, or DELETE incurs additional work to update the index structures.

      StructureRead effectWrite effect
      B-tree indexFaster key lookups (O(logn)O(\log n) vs. O(n)O(n))Must update index on every write
      Materialised viewPrecomputed query results, instant readsMust refresh after base table changes
      ReplicaReads distributed across copiesAll copies must be kept in sync

      OLAP vs. OLTP

      Databases are broadly categorised by their primary workload:

      OLAP: Online Analytical Processing

      CharacteristicDescription
      Write patternWrite once, or journal/ledger updates
      Associated withDecision support, data warehousing
      Data typeHistorical data
      Access patternMostly reads
      OptimisationOptimised for reads
      Data redundancyHigh

      OLTP: Online Transaction Processing

      CharacteristicDescription
      Write patternRich mix of queries and updates to live data
      Associated withDay-to-day operations
      Data typeCurrent data
      Access patternMostly updates
      OptimisationOptimised for updates
      Data redundancyLow

      The ETL pipeline

      Data flows from operational systems to analytical systems through an ETL pipeline:

      OLTP  ----Extract---->  Transform  ----Load---->  OLAP
      (live, normalised)                             (historical, denormalised)
      1. Extract: pull data from OLTP databases.
      2. Transform: clean, reshape, and aggregate the data. This is where denormalisation and redundancy are introduced to optimise for analytical queries.
      3. Load: insert the transformed data into the OLAP system (data warehouse).

      Update history

      An update to a relational database occludes the previous value of a field. The old value is overwritten and lost.

      A revision control system (like Git) stores the update history — an additional dimension. Even for OLTP systems, an update history within a limited time horizon is stored for ACID durability (via write-ahead logs).

      SystemHistorical state
      Relational DBMSOnly current state (previous values overwritten)
      Version controlFull history of every change
      OLTP with WALRecent history maintained for crash recovery

      Further concepts (no longer on syllabus)

      • Data cube model: multi-dimensional views of data for OLAP.
      • Embedded databases (FIDO = Fetch Intensive Data Organisation): read-optimised snapshots extracted from a normalised database, stored on devices for fast local queries.

      Summary

      • Low redundancy optimises for writes; high redundancy optimises for reads.
      • Indexes are a form of controlled redundancy.
      • OLAP systems prioritise reads and use high redundancy.
      • OLTP systems prioritise writes and use low redundancy.
      • ETL pipelines bridge the gap between OLTP and OLAP.
  • Document-Oriented Databases

    Semi-structured data, serialisation with XML and JSON, key-value stores as the simplest document model, document-oriented and aggregate-oriented databases, the NoSQL movement, TinyDB as a document database example, XPath and path-based query languages, the schema-free ideal and its critiques, and branded types as the inverse of semi-structured data

    • Semi-Structured Data

      Structure and the database boundary

      A textbook has clear structure: chapters, sections, figures, cross-references, a table of contents. It would be prohibitively expensive to manually index every word and every structural relationship for database storage. The question is: what can sensibly, or usefully, be stored in a database?

      Two broad approaches exist:

      ApproachDescriptionQuery language
      Store in two partsKeep the document in its native form (LaTeX, Word, PDF); store indexable features in relational tablesSQL
      Store once, perhaps shreddedKeep the document largely in native form (XML, JSON); develop database tools that navigate semi-structured dataXPath, XQuery, application-specific

      The second approach requires the database to return best-effort query answers, since schema violations could be frequent. A relational system would simply reject non-conforming data; a semi-structured system must cope.

      Adding structure to unstructured documents

      When faced with an unstructured corpus, structure can be imposed by:

      1. Human curators — annotate, classify, correct spellings, remove noise.
      2. Automatic tools — keyword extraction, named-entity recognition, classification, indexing.

      The document is carved up and marked up for storage. The original can be reconstructed (un-shredded) as and when necessary.

      Simple approach: keyword-based analysis — bag-of-words indexing, TF-IDF ranking, inverted indices. Fast, well-understood, limited semantic understanding.

      Advanced approach: LLM or NLP-based analysis — entity linking, sentiment analysis, topic modelling, summarisation. More powerful but computationally expensive.

      NLP techniques are not examinable for this course.

      Semi-structured vs. unstructured

      StructuredSemi-structuredUnstructured
      SchemaRigid, enforcedFlexible, implicit, or absentNone
      QueryPrecise (SQL)Best-effortFull-text search
      ExamplesRDBMS tablesXML, JSON documentsRaw text, images, audio

      Why this matters for databases

      Semi-structured data sits between the rigid relational world and the chaos of unstructured text. It acknowledges that real-world data is messy: optional fields, repeated elements, heterogeneous records, evolving schemas. A semi-structured database accepts this messiness and provides tools to navigate it, rather than demanding that data be cleaned and normalised before ingestion.

      Summary

      • Not all data fits neatly into relational tables; some documents are too complex or too loosely structured.
      • Two strategies: (1) pair native documents with relational metadata, (2) store in a flexible format (XML/JSON) with specialised query tools.
      • Structure can be added by humans or automated tools.
      • NLP techniques are out of scope for this course.
    • XML and JSON

      Serialisation

      Serialisation (also called marshalling or pickling) is the process of converting a data structure into a linear sequence of bytes for transfer over a network or for storage in a file.

      • JSON was originally designed for serialising data — lightweight, simple, and easy to parse.
      • XML was designed for both serialisation and marking up human-readable documents so different parts could be located or processed in different ways.
      • CSV is also commonly used for data exchange between databases or applications.
      • NoSQL systems may use XML or JSON as the primary (native) form of a stored document.

      Abstract syntaxes

      Both XML and JSON can be described by abstract tree types:

      type xml_t  = ELEMENT of string * (string * string) ulist * xml_t list
                  | LEAF of string
      
      type json_t = LEAF_S of string
                  | LEAF_N of integer
                  | ARRAY of json_t list
                  | OBJECT of (string * json_t) ulist
                  | NULL

      Both contain tree-structured text with named nodes. They are broadly similar in their representational power, though XML has attributes (the (string * string) ulist on each element) whereas JSON does not distinguish attributes from child nodes.

      The XML structure spectrum

      XML documents can lie anywhere on a spectrum of structural regularity:

      StructureDescription
      All data in one large elementUnstructured blob; XML provides no benefit beyond a wrapper
      Semi-structuredSome elements contain a lot of text, others contain atomic values
      Every atomic value in its own elementFully marked up; unrealistic in practice because of verbosity

      The schema rigorousness spectrum

      RigorousnessDescription
      Precise schema with URLDocument points to a formal schema (DTD, XML Schema) at a known URI
      Relaxed schemaSchema exists but is loosely enforced
      Extra attributes allowedSchema defines a minimum; additional attributes may appear beyond it
      No schema at allDocument is purely self-describing; structure is whatever the programmer believes it to be

      JSON as the dominant format

      JSON has largely displaced XML for new applications. Reasons include:

      • Lighter syntax: fewer angle brackets and closing tags.
      • Direct mapping to native types in JavaScript, Python, and similar languages.
      • Faster parsing.
      • Simpler mental model: objects, arrays, strings, numbers, booleans, null.

      XML retains advantages where markup of document text is needed (mixing tags and prose), where namespaces are important, or where an ecosystem of standardised tools (XSLT, XPath, XQuery) already exists.

      Comparison

      FeatureJSONXML
      Data modelObjects, arrays, scalarsElements with attributes and children
      Schema languageJSON Schema (optional)DTD, XML Schema, Relax NG
      Query languageJSONPath, jqXPath, XQuery
      Mixed contentNot supportedSupported (text interspersed with tags)
      NamespacesNot built inBuilt in
      ReadabilityHigh for dataGood for marked-up documents
    • Key-Value Stores

      The associative store, generalised

      A key-value store generalises the associative array (dictionary) from Lecture 1: values are blobs (arbitrary sequences of bytes), not just strings. Any structure inside the blob is opaque to the key-value store — it is the application’s concern, not the DBMS’s.

      Store:   key ──→ blob
               string   bytes (opaque)

      Opaqueness implies the DBMS knows nothing about what is stored. It would not mind if values were encrypted and it never saw the encryption keys. This is both a strength (simplicity, security) and a limitation (no querying within values, no secondary indices without application effort).

      Distribution

      Many key-value store implementations are distributed: data is spread over all participating machines as shards. The typical mechanism:

      1. Hash the key to produce a shard identifier.
      2. Route the read/write to the machine responsible for that shard.

      This provides:

      BenefitMechanism
      Load balancingHash distributes keys uniformly across shards
      RedundancyEach shard may be replicated across multiple machines for durability
      ScalabilityAdding machines increases total capacity; rebalancing redistributes shards

      ACID vs. BASE semantics

      Implementations can range between full ACID guarantees and relaxed BASE semantics:

      PropertyACID (traditional RDBMS)BASE (many K/V stores)
      AtomicityFull rollback on failureBest-effort; partial writes possible
      ConsistencyInvariants enforced by DBMSApplication-level, eventual
      IsolationSerializable transactionsOften none or read-committed
      DurabilityWrite-ahead log, fsyncReplication to N nodes; quorum writes

      A note on redundancy

      It is important to distinguish two kinds of redundancy:

      KindMeaning
      Physical redundancyMultiple copies of the same data for durability (replication across machines)
      Schema redundancyThe same data stored in multiple places within the logical model (denormalisation)

      Key-value stores provide physical redundancy through replication. Schema redundancy is an application-level choice — the store itself cannot help with or prevent denormalisation, since the blob contents are opaque.

      Use cases

      • Caching (e.g., Redis, Memcached): fast in-memory lookups with optional persistence.
      • Session stores: web session data keyed by session ID.
      • Configuration stores: distributed configuration that must be available across a fleet.
      • Shopping carts: per-user carts that tolerate eventual consistency.

      Summary

      • A key-value store maps string keys to opaque blobs.
      • Distribution provides load balancing, redundancy, and scalability via hashed sharding.
      • ACID vs. BASE is a spectrum; different stores choose different points.
      • Physical redundancy (replication) is distinct from schema redundancy (denormalisation).
    • Document-Oriented Databases and NoSQL

      What is a document database?

      A document-oriented database (also called an aggregate-oriented database) stores data in the form of semi-structured objects — typically JSON or XML documents. Each document is a self-contained aggregate that bundles related data together.

      Denormalised data is not directly semantically related to the key under which it is stored. For example, a movie document might contain embedded arrays of actors, directors, and genres — even though these are conceptually separate entities.

      Why denormalise?

      A denormalised schema enables rapid retrieval: one or two key lookups pull in much or all of the data likely to be needed by the application. Once the document is in memory, all sorts of fast, local operations (filtering, projection, join-like composition) can be performed in an application-specific way.

      Traditional RDBMS (normalised):
        SELECT * FROM movies JOIN cast ON ... JOIN people ON ...
        → Many disk seeks, index traversals
      
      Document DB (denormalised):
        db.movies.find({movie_id: 'tt0111161'})
        → One lookup, all data in hand

      Query languages

      Document databases support diverse query styles:

      Query typeExample
      Unstructured text searchFull-text search over plot summaries, trivia, goofs
      Tag/key-value queriesFind all movies with genre: 'Sci-Fi' and year > 2010
      Application-specific compositionsPython code that iterates over embedded casts, filtering and transforming

      Standards exist — XPath for XML, JSONPath for JSON — but in practice, using general high-level languages to formulate queries was common. The ideal, however, is to write queries in a declarative language: imperative programming can defeat future automated query optimisation.

      Inverted indices and re-normalisation

      The “database” itself may support a variety of inverted indices — mapping from field values back to the documents that contain them — or even re-normalised copies of the data. This enables efficient queries on fields that are not the primary key.

      Key nestings and replication

      To support rapid retrieval using different keys, data may be precomputed and stored under multiple nesting structures. A movie might be stored:

      1. Keyed by movie_id, with cast embedded.
      2. Keyed by person_id, with filmography embedded.

      This replication factor multiplies with any replication already present from denormalisation:

      Total storage=base data×denormalisation factor×nesting replication factor\text{Total storage} = \text{base data} \times \text{denormalisation factor} \times \text{nesting replication factor}

      Document vs. relational

      Relational (RDBMS)Document (NoSQL)
      SchemaEnforced by DBMSImplicit, application-level
      NormalisationEncouraged, formalisedOptional; often denormalised
      JoinsPerformed by DBMS at query timePre-computed by embedding
      Query languageSQL (declarative, standardised)Varied; often programmatic
      Use caseStructured, related data with integrity constraintsSemi-structured aggregates; rapid single-lookup retrieval

      Summary

      • Document databases store denormalised aggregates (JSON/XML documents).
      • Denormalisation trades storage space and write complexity for read performance.
      • Query languages range from declarative standards to imperative application code.
      • Data may be replicated under multiple key nestings to support different access patterns.
      • The replication factor compounds: denormalisation × multiple nestings.
    • TinyDB: A Document Database in Practice

      Overview

      TinyDB is the document database used for the 2nd Assessed Exercise (tick). It is:

      • In-core — all data held in memory; no disk persistence layer.
      • JSON-based — documents are JSON objects, not XML.
      • Queried using Python — no separate query language; queries are Python expressions.
      • No transaction support — no atomicity, consistency, isolation, or durability guarantees beyond what Python provides.

      The absence of transactions makes a distributed/sharded version relatively easy to implement (though this is not done in the course).

      Data model: two denormal tables

      TinyDB provides two primary collections:

      CollectionContents
      MoviesMovie documents with embedded actors, directors, producers, writers, genres
      PeoplePerson documents with embedded filmography (acted in, directed, produced)

      Unstructured text fields include Goofs, Trivia, and Quotes — these are full-text searchable but have no enforced internal structure.

      Structure and indexing

      Data needs to be indexed on various keys. Keys must still be unique, but uniqueness is not enforced by the database — it is a convention the programmer must maintain.

      Some fields are foreign keys (e.g., person_id in a cast list references a person in the People collection). Key integrity is expected but not enforced. The database would not stop you from putting a person object in the Movie collection — the collection names are just names to help the programmer, unlike SQL tables that enforce structure.

      Example records

      Person:

      {
        "person_id": "nm0031976",
        "name": "Judd Apatow",
        "birthYear": "1967",
        "acted_in": [
          { "movie_id": "tt0118715", "title": "The Cable Guy", "role": "Director" }
        ],
        "directed": [
          { "movie_id": "tt0405422", "title": "The 40-Year-Old Virgin", "year": "2005" }
        ]
      }

      Movie (abbreviated):

      {
        "movie_id": "tt0111161",
        "title": "The Shawshank Redemption",
        "year": "1994",
        "genres": ["Drama"],
        "actors": [
          { "person_id": "nm0000209", "name": "Tim Robbins", "role": "Andy Dufresne" }
        ],
        "directors": [
          { "person_id": "nm0001104", "name": "Frank Darabont" }
        ]
      }

      Querying

      Queries are written in Python:

      # Find a person by ID
      tdb_people.get(Query().person_id == 'nm0000002')
      
      # Find movies by year
      tdb_movies.search(Query().year == '1994')
      
      # Compound query
      tdb_movies.search((Query().year == '1994') & (Query().genres.any('Drama')))

      Considerations for the assessed exercise

      When implementing the tick, think about:

      • Query planning: can you avoid scanning the entire collection for every query?
      • Cost of correcting a systematically misspelled actor’s name: in a document database, you must update every movie document that embeds that actor, plus the actor’s own person record. Compare with an RDBMS where a single update suffices.
      • Cost of joins: in TinyDB, joins are done by the application, not the DBMS. What is the performance implication?
      • Insertion checks: what should be validated when inserting new data? Key uniqueness? Referential integrity? Data type conformance?
      • ACID properties: which ones might be relatively easy to implement in TinyDB, and which would be hard?

      Summary

      • TinyDB is an in-core, JSON-based, Python-queried document database with no transaction support.
      • Two denormalised collections: Movies and People.
      • No schema enforcement; collection names are conventions.
      • Queries are Python expressions against a Query() builder.
      • The exercise tests understanding of the trade-offs between document and relational models.
    • The Schema-Free Ideal and Branded Types

      The NoSQL schema-free ideal

      The “schema-free” ideal of NoSQL is sometimes described as a grail: a database that imposes no structural constraints on the data it stores. In practice, “no schema” really means:

      The schema is not stored as part of the database, nor checked during update.

      For most activities, there will inevitably still be a schema — perhaps on a whiteboard, a scrap of paper, or stored in somebody’s head. New joiners to a software project must learn this schema somehow; the DBMS provides no assistance.

      The cost of schema absence

      AspectWith schema (RDBMS)Without schema (NoSQL)
      DiscoveryDESCRIBE TABLE; system cataloguesRead the code; ask a colleague
      EnforcementDBMS rejects invalid writesInvalid writes succeed silently
      ToolingSchema-aware editors, ORMs, code generationManual, ad-hoc
      EvolutionFormal migrations (ALTER TABLE)Application-level versioning of documents

      Dynamic typing and its trajectory

      The commercial success of dynamically-typed languages (JavaScript, Ruby, Python, PHP) is notable, but the trend is changing:

      • JavaScript is increasingly a compilation target, being displaced by WebAssembly (WASM).
      • Python types are now used de rigueur, pioneered by J. Lehtosalo (of this department) with mypy.
      • TypeScript has become the dominant way to write JavaScript at scale.
      • The industry is moving toward gradual typing: add types where they help, leave them out where they do not.

      Branded types

      Branded types represent the opposite end of the spectrum from semi-structured data. Databases hold many strings and numbers, many of which are members of enumerations or have units of measure (UoM). Should we make these types overt?

      type velocity_t = branded float
      type distance_t = branded float
      type duration_t = branded int

      Without branded types:

      journey_time: int = 45        # minutes
      bognor_to_romsey: int = 62    # miles
      result = journey_time + bognor_to_romsey  # = 107 — dimensionally unsound!

      The type system would not catch this error. With branded types:

      journey_time: duration_t = 45
      bognor_to_romsey: distance_t = 62
      result = journey_time + bognor_to_romsey  # Type error!

      Branded types prevent silly operations on data by tracking the semantic category of each value through the type system.

      Being a key is a sort of type

      When a value serves as the key to another table, that relationship itself is a form of typing. A person_id is not just any string — it is a string that must correspond to an entry in the People collection. This is referential integrity, and it is a type constraint that the DBMS could enforce (as RDBMSs do) or leave to the application (as document databases do).

      Summary of the NoSQL / semi-structured area

      ThemeObservation
      Standards churnRDF, OWL, YAML, SOAP, XMLRPC, JSON — many competing standards over two decades
      Data size inflationHuman-readable representations have led to an order-of-magnitude increase in data size and parsing overhead compared with binary data exchange
      ConvergenceMany traditional SQL systems were extended with NoSQL features (JSON columns, document storage); many NoSQL systems added SQL-like query languages and transactions
      Tripos noteFor document database questions, a well-argued answer can garner full credit even if it disagrees with the expected answer

      Summary

      • “Schema-free” means the schema is not stored in or enforced by the DBMS — not that no schema exists.
      • New joiners must learn the schema from documentation, code, or colleagues.
      • The industry is trending toward gradual typing and away from pure dynamic typing.
      • Branded types encode semantic categories (enumerations, units of measure) into the type system to prevent dimensionally unsound operations.
      • Relational and document approaches are converging, not diverging.
  • Further SQL

    The computational complexity of joins and how database indexes reduce it, multisets (bags) versus sets and why SQL defaults to bags, GROUP BY and aggregate functions (min, max, avg, count), NULL and three-valued logic with its pitfalls and controversies, function and relation composition, Bacon numbers and the co-actor relation, transitive closure and why it cannot be expressed in plain Relational Algebra, and recursive SQL with the WITH RECURSIVE construct

    • Join Complexity and Database Indexes

      Brute force join

      Given relations R(A,B)R(A, B) and S(B,C)S(B, C), the naive approach to compute RSR \bowtie S scans RR, and for each tuple in RR, scans all of SS. In the worst case this requires on the order of R×S|R| \times |S| steps:

      TbruteRST_{\text{brute}} \propto |R| \cdot |S|

      This is quadratic in the table sizes and becomes infeasible for large tables.

      The common case

      In practice, on each iteration over RR, there may be only a very small number of matching records in SS. If RR‘s BB is a foreign key referencing S(B)S(B), there is exactly one matching tuple. The brute force strategy still scans all of SS to find it.

      Indexes

      An index is a data structure that can greatly reduce the time needed to locate records matching a given key. With an index on S(B)S(B), the inner loop becomes a lookup on S-INDEX-ON-B(b):

      TindexedRlogST_{\text{indexed}} \propto |R| \cdot \log |S|

      This reduces the inner loop from S\propto |S| (linear scan) to logS\propto \log |S| (index lookup). For large tables, the difference is enormous.

      SQL syntax

      CREATE INDEX index_name ON S(B);
      DROP INDEX index_name;

      Index types and considerations

      There are many types of database indexes (B-trees, hash indexes, bitmap indexes, GIN, GiST, and so on). The SQL standard does not define index creation; the syntax varies between implementations. Index creation is a physical design concern and may be handled by a specialist DBA team or by automated tools in modern systems.

      The fundamental trade-off

      OperationEffect of indexes
      Read (SELECT)Faster (lookup instead of scan)
      Write (INSERT, UPDATE, DELETE)Slower (index must be updated)

      This is a fundamental database trade-off: every index speeds up queries that use it, but imposes a cost on every write operation. Choosing which indexes to create requires understanding the workload’s balance of reads and writes.

      Underlying data structures

      The Ia Algorithms course presents the data structures used to implement indexes:

      • Search trees (B-trees and variants): support range queries and ordered access.
      • Hash tables: provide O(1)O(1) average-case equality lookups but no range queries.

      A database may maintain multiple indexes per table, each optimised for different query patterns.

      Summary

      • Without indexes, RSR \bowtie S is RS\propto |R| \cdot |S|.
      • An index on the join column reduces the inner loop to logS\propto \log |S|.
      • Indexes speed reads but slow writes — a fundamental trade-off.
      • Underlying data structures: search trees, hash tables.
    • Multisets (Bags) and DISTINCT

      Sets vs. multisets

      The Relational Algebra operates over sets: each tuple appears at most once. Duplicates are automatically eliminated.

      SQL is actually based on multisets (bags): a tuple may appear multiple times. The query:

      SELECT B, C FROM R;

      produces a bag. If RR has rows (20, 10, 0, 55) and (11, 10, 0, 7), the result shows (10, 0) twice.

      DISTINCT

      The DISTINCT keyword brings SQL back to set semantics:

      SELECT DISTINCT B, C FROM R;

      This eliminates duplicate rows from the output, returning each unique (B, C) pair exactly once.

      Why multisets?

      Duplicates are essential for aggregate functions (MIN, MAX, AVG, COUNT, SUM, and so on). Consider a marks table:

      sidcoursemark
      s1Maths75
      s2Maths82
      s3Maths68
      s1Physics90
      s2Physics85

      Without keeping duplicates, counting the number of students per course and computing averages would be impossible:

      SELECT course, COUNT(*), AVG(mark)
      FROM marks
      GROUP BY course;
      courseCOUNT(*)AVG(mark)
      Maths375.0
      Physics287.5

      GROUP BY

      The GROUP BY construct partitions rows into groups based on equality of specified columns. Each group is then reduced by aggregate functions to a single row in the output.

      The mental model:

      1. Partition the table by the GROUP BY column(s).
      2. Within each group, apply the aggregate functions.
      3. Output one row per group.

      Summary

      • RA works on sets; SQL works on multisets (bags).
      • DISTINCT restores set semantics by eliminating duplicates.
      • Aggregation requires duplicates — without them, counting and averaging are impossible.
      • GROUP BY groups rows; aggregate functions reduce each group to a single value.
    • GROUP BY and Aggregate Functions

      GROUP BY

      The GROUP BY construct partitions rows into groups. For each group, aggregate functions reduce the group to a single scalar value:

      SELECT course, MIN(mark), MAX(mark), AVG(mark)
      FROM marks
      GROUP BY course;

      This returns one row per course with the minimum, maximum, and average mark for that course.

      Aggregate functions

      FunctionDescription
      MIN(expr)Minimum value in the group
      MAX(expr)Maximum value in the group
      AVG(expr)Arithmetic mean of non-NULL values
      COUNT(expr)Number of non-NULL values
      COUNT(*)Number of rows (including NULLs)
      SUM(expr)Sum of non-NULL values

      SELECT clause restrictions

      The SELECT clause can only contain:

      1. Columns that appear in the GROUP BY clause.
      2. Expressions involving aggregate functions.

      Columns not in GROUP BY and not wrapped in an aggregate are illegal, because within a group there may be multiple values for that column — which one would be selected?

      HAVING

      The HAVING clause filters groups, just as WHERE filters rows, but applied after grouping:

      SELECT course, AVG(mark) AS avg_mark
      FROM marks
      GROUP BY course
      HAVING AVG(mark) >= 70;
      ClauseFiltersApplied
      WHEREIndividual rowsBefore grouping
      HAVINGGroupsAfter grouping

      Order of execution

      The logical order of execution for a SELECT query with grouping:

      1. FROM — identify the source table(s)
      2. WHERE — filter rows
      3. GROUP BY — partition into groups
      4. HAVING — filter groups
      5. SELECT — project columns and compute aggregates
      6. ORDER BY — sort the result

      Note that column aliases defined in the SELECT clause are not visible in WHERE or GROUP BY.

      Properties of aggregates

      The reduction operator must be commutative and associative so that the grouping order does not affect the result. All standard aggregate functions satisfy this:

      f(a,b)=f(b,a)(commutative)f(a, b) = f(b, a) \quad \text{(commutative)} f(a,f(b,c))=f(f(a,b),c)(associative)f(a, f(b, c)) = f(f(a, b), c) \quad \text{(associative)}

      This guarantees that whether the database processes groups in parallel or serially, the result is deterministic.

      COUNT and NULLs

      COUNT(*) counts all rows, including those with NULL values. COUNT(column) counts only non-NULL occurrences of that column:

      SELECT COUNT(*), COUNT(mark) FROM marks;

      If some mark values are NULL, these two counts differ.

      Summary

      • GROUP BY partitions rows; aggregates reduce each group to a scalar.
      • SELECT may only reference grouping columns or aggregates.
      • HAVING filters groups after grouping; WHERE filters rows before grouping.
      • Execution order: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY.
      • COUNT(*) includes NULLs; COUNT(column) excludes them.
      • Aggregate functions are commutative and associative, enabling parallel evaluation.
    • NULL and Three-Valued Logic

      What NULL is not

      NULL is not the empty string, zero, or the boolean false. NULL is a place-holder, not a value. NULL is not a member of any domain (type).

      Three-valued logic

      Because NULL represents “we don’t know”, we need three-valued logic (3VL). Let \bot (or UNKNOWN) represent the third truth value. The truth tables extend Boolean logic:

      AND (\land)

      \landTTFF\bot
      TTTTFF\bot
      FFFFFFFF
      \bot\botFF\bot

      OR (\lor)

      \lorTTFF\bot
      TTTTTTTT
      FFTTFF\bot
      \botTT\bot\bot

      NOT (¬\neg)

      ¬\negResult
      TTFF
      FFTT
      \bot\bot

      Key observations:

      • F=F\bot \land F = F (one false makes the conjunction false, regardless of unknowns).
      • T=T\bot \lor T = T (one true makes the disjunction true).
      • ¬=\neg \bot = \bot (the negation of unknown is unknown).

      Unexpected NULL behaviour

      NULL can lead to surprising results:

      SELECT * FROM students WHERE age <> 19;

      This does not return rows where age IS NULL. Any comparison with NULL yields \bot, and only rows where the WHERE clause evaluates to TT are included in the result. A row where age IS NULL evaluates age <> 19 to \bot, which is not TT, so the row is excluded.

      Interpretations of NULL

      There are several possible interpretations of NULL, creating what the lecture calls “a great deal of semantic muddle”:

      InterpretationMeaning
      Missing valueThere is a value, but we don’t know what it is
      InapplicableNo value is applicable (e.g., spouse for an unmarried person)
      Hidden valueThe value exists but you are not allowed to see it

      SQL’s definition

      SQL’s definition: “NULL is not equal to anything — not even to another NULL.” This avoids the “common sister” paradox where two rows with unknown values could be spuriously equated.

      The IS NULL predicate

      IS NULL is the standard way to test for NULL. Unlike comparisons, IS NULL evaluates to either TT or FF — never \bot:

      SELECT * FROM students WHERE age IS NULL;

      Similarly, IS NOT NULL returns TT or FF.

      The NULL controversy

      C.J. Date (a prominent relational theorist) argues that NULLs and 3VL are “a serious mistake” that “have no place in the relational model.” His criticism: NULLs violate the relational model’s logical foundations and create subtle inconsistencies.

      Defender argument: NULLs serve an important practical role in handling incomplete information. The flaws should be repaired rather than abandoning NULLs entirely.

      SQL inconsistency: GROUP BY on NULLs

      Consider a GROUP BY on a column that contains NULLs:

      SELECT col, COUNT(*) FROM t GROUP BY col;

      SQL treats all NULL values as equivalent for grouping purposes — they form a single group. This contradicts the principle that “NULL is not equal to anything — not even to another NULL.” Under the rule that NULLNULL\text{NULL} \neq \text{NULL}, each NULL would belong to its own group, but SQL groups them together.

      Summary

      • NULL is a place-holder, not a value, and not a member of any domain.
      • Three-valued logic extends boolean truth tables with \bot (UNKNOWN).
      • Comparisons with NULL produce \bot; only TT satisfies WHERE.
      • Use IS NULL / IS NOT NULL to test for NULL reliably.
      • NULL has multiple interpretations: missing, inapplicable, or hidden.
      • C.J. Date criticises NULLs as a relational model violation.
      • GROUP BY inconsistently treats all NULLs as one group.
    • Function and Relation Composition

      Function composition

      Given functions ff and gg, function composition is written (fg)(x)=f(g(x))(f \circ g)(x) = f(g(x)). If g(x)=yg(x) = y and f(y)=zf(y) = z, then (fg)(x)=z(f \circ g)(x) = z.

      The domain of fgf \circ g is those xx in the domain of gg for which g(x)g(x) lies in the domain of ff.

      Relation composition

      Given relations RS×TR \subseteq S \times T and QT×UQ \subseteq T \times U, their composition is:

      QRS×UQ \circ R \subseteq S \times U

      QR{(s,u)tT.  (s,t)R(t,u)Q}Q \circ R \equiv \{(s, u) \mid \exists t \in T.\; (s, t) \in R \land (t, u) \in Q\}

      We read QRQ \circ R as ”QQ after RR”: first follow RR from ss to some tt, then follow QQ from tt to uu. The intermediate element tt is existentially quantified and does not appear in the result.

      Partial functions as relations

      A relation RS×TR \subseteq S \times T defines a (partial) function when:

      (s,t1)R(s,t2)R    t1=t2(s, t_1) \in R \land (s, t_2) \in R \implies t_1 = t_2

      That is, each ss is related to at most one tt. This is the univalence (or functional) property.

      A relation uniquely defines a total function if additionally every sSs \in S has some tt with (s,t)R(s, t) \in R (the relation is left-total).

      When mathematicians say “function” they typically mean a total function. A relation may give multiple answers for the same input; a partial function may give at most one; a total function gives exactly one.

      PropertyRelationPartial functionTotal function
      Univalued (at most one output)Not requiredRequiredRequired
      Left-total (at least one output)Not requiredNot requiredRequired

      Composition of functions is a special case

      If RR and QQ are functions (univalued and left-total), then QRQ \circ R as defined by relation composition coincides with ordinary function composition. The existential quantification t\exists t is equivalent to function application because tt is uniquely determined.

      Joins as generalised composition

      If we write QRQ \circ R in join notation, identifying the TT-component as the join column:

      R2=1QR \bowtie_{2=1} Q

      We see that joins are a generalisation of function composition. The generalisation works in two directions:

      1. Functions to relations: joins cope with relations that are not univalued (multiple outputs per input) and not left-total (some inputs have no output).
      2. Equi-join to natural join: generalises from joining on a positional column to joining on named attributes.

      Example

      Let R(student,course)R(\text{student}, \text{course}) represent enrolment, and Q(course,lecturer)Q(\text{course}, \text{lecturer}) represent who teaches each course. Then QRQ \circ R gives (student,lecturer)(\text{student}, \text{lecturer}) pairs: students and the lecturers of courses they take.

      In SQL:

      SELECT R.student, Q.lecturer
      FROM R
      JOIN Q ON R.course = Q.course;

      If a student takes multiple courses, they appear with multiple lecturers. If a course has no lecturer (not left-total), the student for that course contributes no row to the result under an inner join.

      Summary

      • Function composition: (fg)(x)=f(g(x))(f \circ g)(x) = f(g(x)).
      • Relation composition: QR={(s,u)t.  (s,t)R(t,u)Q}Q \circ R = \{(s, u) \mid \exists t.\; (s, t) \in R \land (t, u) \in Q\}.
      • Functions are univalued relations; total functions are also left-total.
      • Function composition is a special case of relation composition.
      • Joins (RQR \bowtie Q) are a generalisation of relation composition, further generalised by named attributes and outer join variants.
    • Bacon Numbers and Transitive Closure

      Bacon numbers

      Kevin Bacon has Bacon number 0. Any actor who has appeared in a movie with Kevin Bacon has Bacon number 1. For any other actor, their Bacon number is k+1k+1 where kk is the smallest Bacon number among their co-actors.

      More generally, this is the shortest-path distance in a graph where vertices are actors and edges connect actors who have appeared together in a film.

      The co-actors view

      First, build a relation of pairs of actors who have appeared in the same movie:

      CREATE VIEW coactors AS
      SELECT DISTINCT p1.person_id AS pid1, p2.person_id AS pid2
      FROM plays_role AS p1
      JOIN plays_role AS p2 ON p2.movie_id = p1.movie_id;

      This relation is:

      • Reflexive: every actor co-acts with themselves (they appear in the same movie as themselves). The query above includes this because p1p1 can equal p2p2 when p1.movie_id = p2.movie_id.
      • Symmetric: if A co-acts with B, then B co-acts with A (achieved because the join generates both orderings).

      Computing Bacon numbers in SQL (without recursion)

      Without recursive SQL, we must hard-code each level. For Bacon number 1:

      CREATE VIEW bacon_number_1 AS
      SELECT DISTINCT pid2 AS pid
      FROM coactors
      WHERE pid1 = (SELECT person_id FROM people WHERE name = 'Kevin Bacon');

      For Bacon number 2, join the level-1 actors with coactors, then exclude those already found:

      CREATE VIEW bacon_number_2 AS
      SELECT DISTINCT c.pid2 AS pid
      FROM bacon_number_1 AS b1
      JOIN coactors AS c ON c.pid1 = b1.pid
      WHERE c.pid2 NOT IN (SELECT pid FROM bacon_number_1)
        AND c.pid2 <> (SELECT person_id FROM people WHERE name = 'Kevin Bacon');

      Each subsequent level follows the same pattern: join the previous level’s actors with coactors, exclude actors already found at earlier levels (to avoid counting longer paths), and exclude Kevin Bacon.

      The final view unions all levels:

      CREATE VIEW bacon_numbers AS
      SELECT pid, 0 AS n FROM people WHERE name = 'Kevin Bacon'
      UNION
      SELECT pid, 1 AS n FROM bacon_number_1
      UNION
      SELECT pid, 2 AS n FROM bacon_number_2
      ...
      UNION
      SELECT pid, 9 AS n FROM bacon_number_9;

      Transitive closure

      The transitive closure R+R^+ of a binary relation RR is the smallest binary relation such that:

      1. RR+R \subseteq R^+ (it contains RR)
      2. R+R^+ is transitive: (x,y)R+(y,z)R+    (x,z)R+(x, y) \in R^+ \land (y, z) \in R^+ \implies (x, z) \in R^+

      The transitive closure can be expressed as the union of powers:

      R+=n{1,2,}RnR^+ = \bigcup_{n \in \{1,2,\ldots\}} R^n

      where R1=RR^1 = R and Rn+1=RnRR^{n+1} = R^n \circ R.

      Finiteness and the limitation of RA

      Since all our relations are finite, there must exist some kk such that:

      R+=RR2RkR^+ = R \cup R^2 \cup \cdots \cup R^k

      However, kk depends on the contents of RR. The relational algebra cannot express “union up to whatever power is needed” because RA expressions have fixed, finite length. Therefore, transitive closure cannot be computed in the Relational Algebra (equivalently, in SQL without recursion).

      Graph model

      A directed graph G=(V,A)G = (V, A) has vertices VV and arcs AV×VA \subseteq V \times V, where AA is a binary relation over VV.

      The RR-distance (hop count) from s0s_0 to ss' is the least nn such that:

      (s0,s)Rn(s_0, s') \in R^n

      If no such nn exists, the distance is undefined (the vertices are not connected).

      Bacon number is the RR-distance from Kevin Bacon in the co-actor relation.

      Summary

      • Bacon number is the shortest-path distance in the co-actor graph.
      • Without recursion, each Bacon level requires a separate hard-coded view.
      • The transitive closure R+=nRnR^+ = \bigcup_{n} R^n is the smallest transitive relation containing RR.
      • kk depends on the data; RA cannot express “enough unions,” so transitive closure is inexpressible in RA.
      • Directed graphs model binary relations; RR-distance is the least nn with (s0,s)Rn(s_0, s') \in R^n.
    • Recursive SQL (WITH RECURSIVE)

      The WITH keyword

      The WITH keyword in SQL allows a recursive declaration: a query that refers to its own name. This enables expressing computations that would otherwise require an unbounded number of RA expressions.

      Non-terminating recursion

      A naive recursive query has no fixed point and runs forever:

      WITH R AS (
        SELECT 1 AS n
      )
      SELECT n + 1 FROM R;

      This generates 1, 2, 3, … indefinitely. Real recursive CTEs require a base case and a termination condition.

      Bounded recursion with a base case

      WITH countUp AS (
        SELECT 1 AS n                   -- base case
        UNION ALL
        SELECT n + 1 FROM countUp       -- recursive step
        WHERE n < 3                     -- termination condition
      )
      SELECT * FROM countUp;

      Result:

      n
      1
      2
      3

      The recursive CTE works as follows:

      1. Evaluate the base case (seed: n = 1).
      2. Apply the recursive step to the result of the previous iteration.
      3. Add the new rows to the result.
      4. Repeat from step 2 until the recursive step produces no new rows.

      RECURSIVE keyword

      SQL:1999 introduced WITH RECURSIVE. Some implementations require the keyword; others infer recursion from the self-reference.

      Bacon numbers with recursion

      The Bacon number problem can be solved cleanly with a recursive CTE:

      WITH RECURSIVE bacon(n, pid) AS (
        SELECT 0 AS n, pid2 AS pid
        FROM coactors
        WHERE pid1 = 'nm0000102'    -- Kevin Bacon's person_id
          AND pid1 = pid2           -- ensures reflexivity for Bacon himself
      
        UNION
      
        SELECT n + 1 AS n, c.pid2 AS pid
        FROM bacon
        JOIN coactors AS c ON c.pid1 = bacon.pid
        WHERE NOT (c.pid2 IN (SELECT pid FROM bacon))  -- exclude already-found actors
          AND n < 20                                    -- safety limit
      )
      SELECT n, COUNT(*)
      FROM (
        SELECT MIN(n) AS n, pid
        FROM bacon
        GROUP BY pid
      ) AS shortest
      GROUP BY n;

      Key elements of this query:

      ElementPurpose
      Base case n = 0Seeds the recursion with Kevin Bacon
      AND pid1 = pid2Ensures Bacon has Bacon number 0 (reflexive self-co-acting)
      UNION (not UNION ALL)Avoids duplicates that could cause infinite loops
      NOT (c.pid2 IN (SELECT pid FROM bacon))Prevents revisiting actors; ensures shortest path is found
      n < 20Safety limit; prevents infinite recursion on pathological data
      Outer MIN(n)For actors reachable via multiple paths, picks the shortest

      Graph databases

      As noted in the lecture, computing Bacon numbers is much easier in a graph database (covered in Lecture 8), where shortest-path queries are a primitive operation. The recursive SQL approach is instructive for understanding transitive closure but is not the most natural tool for the job.

      Recursive CTE mechanics

      A recursive CTE has this general form:

      WITH RECURSIVE cte_name(columns) AS (
        <non-recursive base query>
        UNION [ALL]
        <recursive query referencing cte_name>
      )
      SELECT ... FROM cte_name;
      VariantBehaviour
      UNIONEliminates duplicates each iteration (safer, avoids cycles)
      UNION ALLPreserves duplicates (faster, but risks infinite loops if not careful)

      Summary

      • WITH RECURSIVE enables expressing computations requiring transitive closure in SQL.
      • A recursive CTE has a base case, a recursive step, and (ideally) a termination condition.
      • n < 20 is a safety limit to prevent infinite recursion.
      • The Bacon number query shows a complete recursive CTE: base, recursion, deduplication, and shortest-path extraction.
      • Graph databases handle shortest-path queries more naturally than recursive SQL.
  • Graph Databases

    The graph database model with typed nodes and edges, why storing graphs in relational tables is inefficient, Neo4j and the Cypher query language, pattern matching for path-oriented queries, graph algorithms (BFS, DFS, shortest path, PageRank), and the convergence between graph and relational systems

    • The Graph Database Model

      What a graph database stores

      A graph database stores one big graph, which is essentially an instance of an E/R diagram. The graph is composed of nodes and directed edges.

      Nodes

      Nodes have a type, a unique label (or several, in Neo4j), and properties (key-value pairs). The type corresponds roughly to an entity set in the E/R model. Labels in Neo4j can serve as a lightweight classification mechanism — a node may carry multiple labels, which is useful for tagging without duplicating data.

      Edges

      Edges are directed between two nodes. Each edge has:

      • A type (the name of the relationship).
      • An optional label.
      • Properties (key-value pairs).

      An edge type models an E/R binary (or unary) relation. Since edges connect exactly two nodes, ternary relationships in the E/R model must be handled by other means (see a later note).

      Edges, when created, need have no identifiers. This means that creating the same edge twice between the same two nodes is not idempotent in some systems — two distinct edges with the same type and properties can exist. This contrasts with the relational model, where a tuple in a relation is uniquely identified by its key, and inserting the same tuple twice is either rejected or treated as a duplicate.

      Direction in storage vs. direction in queries

      Edges are stored with a direction, but queries can treat them as undirected. Cypher’s (a)--(b) syntax matches edges in either direction, ignoring the stored orientation. This is important because many real-world relationships (friendship, co-authorship) are symmetric, but the DBMS still requires a direction at storage time.

      Collating by type for relational conversion

      Data from a graph database can be collated by type to produce RDBMS tables. For each node type, one table is formed with columns for each property. For each edge type, one table is formed with columns for the source node reference, target node reference, and each property. This is the inverse of the E/R-to-relational mapping covered earlier in the course.

      Natural problem domains

      The graph model is a natural representation for many real-world problems:

      DomainNodesEdges
      Social networksPeopleFriendships, follows
      Metabolic pathwaysCompounds, enzymesReactions
      Road networksJunctions, townsRoad segments
      Dependency graphsSoftware packagesDepends-on
      The WebPagesHyperlinks
      Organisational chartsEmployeesReports-to

      Regular expression matching and atomicity

      Graph query languages often support regular expression matching on property values. This raises the question of whether it violates value atomicity — the principle that values should not have internal structure visible to queries. In the relational world, WHERE name LIKE 'Sm%' is accepted practice despite performing pattern matching on the internal characters of a string. Graph databases do the same: regular expression matching on values is analogous to LIKE predicates in SQL. The question of whether this breaks the relational model’s first normal form is largely academic; in practice, both models allow it.

      Reusable queries

      Queries in graph databases can be expressed as reusable functions with formal parameters. This is equally possible in RDBMSs (stored procedures, parameterised views, user-defined functions). The graph model does not have a monopoly on encapsulation.

      Security

      Graph databases have suffered serious security vulnerabilities (e.g., injection attacks, authentication bypasses) over the years. This is not inherent to the graph model; it reflects the immaturity of implementations. RDBMSs have had decades of hardening, but they too have suffered SQL injection and privilege-escalation vulnerabilities. The security posture of a DBMS depends on its engineering, not its data model.

      Summary

      • A graph database stores a typed, labelled, directed graph — essentially a running instance of an E/R diagram.
      • Edges are directed in storage but can be queried as undirected.
      • Edge creation is not necessarily idempotent (no inherent key constraint on edges).
      • The graph model maps naturally to domains with networked structure.
      • Regular expression matching in graph queries is analogous to LIKE clauses in SQL.
      • Queries can be parameterised and reused in both graph and relational systems.
    • Storing Graphs in Relational Tables

      The one-big-graph approach

      A simple approach to storing a graph in relational tables uses two tables:

      NODES(Town, Country) — one row per node, with type-specific attributes.

      EDGES(EID, V1, V2, Form, Distance) — one row per edge, where V1 and V2 reference nodes.

      This is a unary relation (endorelation): the schema range and domain type are both towns. Edges go from one town to another, so both ends draw from the same domain.

      Inefficiency of the flat edges table

      To find the neighbours of a given node using the EDGES table requires a full table scan:

      SELECT V2 FROM EDGES WHERE V1 = 'Cambridge';

      For undirected searches, both columns must be examined:

      SELECT V1 AS neighbour FROM EDGES WHERE V2 = 'Cambridge'
      UNION
      SELECT V2 AS neighbour FROM EDGES WHERE V1 = 'Cambridge';

      This is linear in the number of edges. To avoid scanning, two inverted indexes are needed: one on V1 and one on V2. These indexes let the DBMS look up edges by either endpoint efficiently, but they double the storage overhead for the edges table.

      Multi-hop queries in SQL

      Queries involving many hops are painful to express in SQL. Finding all towns reachable from a starting town (the transitive closure, or Kleene star from the Algorithms course) requires recursive Common Table Expressions (CTEs):

      WITH RECURSIVE Reachable AS (
        SELECT V2 AS town FROM EDGES WHERE V1 = 'Cambridge'
        UNION
        SELECT E.V2 FROM EDGES E
        JOIN Reachable R ON E.V1 = R.town
      )
      SELECT * FROM Reachable;

      Each recursive step scans more rows. The query cost grows with the diameter of the graph. For graphs with millions of nodes and long paths, this becomes impractical. Graph databases optimise path traversal internally, but relational systems with recursive CTEs can still be competitive on moderate-sized graphs.

      Bipartite approach: separate node tables

      When the graph is bipartite (edges go between different types), separate node tables are cleaner:

      TOWNS(Town, Population) LANGUAGES(Language, Core_Vocab, Genders) OFFICIAL_LANGUAGE(Town, Language)

      The OFFICIAL_LANGUAGE relation is bipartite: each edge goes from a town to a language. This is identical to the relational representation of a many-to-many relationship covered in the E/R-to-relational mapping notes. The graph model adds nothing that cannot be done in relations.

      RDBMS limitations for enormous relations

      For enormous, many-to-many relations, RDBMSs are not ideal if the primary workload is graph traversal. A join between two large tables to find connected nodes incurs index lookups, and for path-finding queries the relational engine must iterate. Graph databases can store adjacency information more compactly and traverse it more directly.

      For OLAP (analytical) workloads, a denormalised representation would probably be used instead — pre-joining the data into a wide table or a star schema, trading storage space for query speed.

      Ternary relations

      Modelling ternary relations is awkward in a graph, because edges have exactly two ends. Two approaches exist:

      ApproachDescriptionDrawback
      Edge attributesStore the third entity’s attributes as properties on the edgeLoses identity of the third entity; cannot easily join to it
      ReificationTurn the ternary relationship into a node, with three edges connecting to the participantsBlurs the distinction between entities and relationships; more complex queries

      For example, a ternary relationship TEACHES(Lecturer, Course, Term) could be modelled as a node of type TeachingEvent with edges to Lecturer, Course, and Term. This is sometimes called reification — treating a relationship as an entity in its own right.

      Summary

      • Storing graphs in relational tables is straightforward but inefficient for multi-hop traversal without indexes.
      • Two inverted indexes on the source and target columns avoid full scans for neighbour lookups.
      • Recursive CTEs can express transitive closure, but the cost grows with graph diameter.
      • Bipartite graphs map cleanly to standard relational many-to-many representations.
      • Ternary relations are awkward in the graph model; reification (making the relationship a node) is the usual workaround.
    • Neo4j and Cypher

      Neo4j overview

      Neo4j is a Java-based graph-oriented DBMS. It stores data as nodes and directed edges, both with types, labels, and properties. The native query language is Cypher (named after a character in The Matrix). Data is typically imported from external sources rather than entered interactively.

      Creating nodes

      Nodes are created with the CREATE statement. The syntax (identifier:Type {properties}) specifies a node:

      CREATE (nm0000102:Person {name: 'Kevin Bacon', birthyear: 1958})
      • nm0000102 is a variable name used within the query to refer to this node. It is not stored — it is a temporary handle for the duration of the current Cypher statement.
      • Person is the type.
      • The key-value pairs are properties.

      Creating edges

      Edges are created using an arrow syntax between two nodes:

      CREATE (nm0002002)-[:ACTED_IN {role: 'James Bond'}]->(tt0299478)

      This assumes nm0002002 and tt0299478 already exist (they would have been created in earlier CREATE statements). The edge:

      • Has type ACTED_IN.
      • Has property role with value 'James Bond'.
      • Is directed from the actor node to the movie node.

      Nodes and edges both use the <variable name>:<type> syntax. All edges have a stored direction.

      Graph data normalisation: modelling decisions

      Modelling decisions matter in a graph database, just as they do in a relational one. Consider a film database.

      Poor design — using the role name as the edge type:

      (nm0000084)-[:SU_LI_ZHEN]->(tt0212712)

      This is problematic for two reasons:

      1. Edge type names must be unique identifiers. The same role name might appear in remakes with different actors.
      2. It conflates a property of the relationship (the character played) with the type of the relationship (that it is a performance).

      Better design — using a generic edge type and storing the role as a property:

      (nm0000084)-[:PLAYS_ROLE {role: 'Su Li-zhen'}]->(tt0212712)

      Now PLAYS_ROLE is the reusable edge type, and the specific character name is data. This distinction is the graph equivalent of choosing between an attribute and a separate relation in the E/R model.

      Property types

      Neo4j supports a limited set of property types: integers, floats, strings, booleans, and arrays of these. There is no date type, no decimal/numeric type, and no branded types. Dates are conventionally stored as formatted strings (ISO 8601), which loses type safety.

      Importing data

      Neo4j is not typically used for data entry. Data is imported in bulk from CSV, JSON, or other structured sources. The LOAD CSV command reads external files:

      LOAD CSV WITH HEADERS FROM 'file:///people.csv' AS row
      CREATE (:Person {name: row.name, birthyear: toInteger(row.birthyear)})

      This batch-loading pattern is common: graphs are derived from existing datasets rather than built up interactively.

      Comparison to SQL

      AspectSQL (DDL + DML)Cypher
      Node/row creationINSERT INTO T VALUES (...)CREATE (:Type {key: val})
      Edge/relationshipForeign key constraint + join tableCREATE (a)-[:TYPE]->(b)
      Schema enforcementStrong; CREATE TABLE defines columnsWeak; node types are informal
      Path traversalRecursive CTE()-[:TYPE*]->()

      Summary

      • Neo4j stores typed, labelled, directed graphs with key-value properties on nodes and edges.
      • Cypher uses CREATE for both nodes and edges, with (:Type {props}) and -[:Type {props}]-> syntax.
      • Good modelling separates the edge type (what kind of relationship) from edge properties (the data about a particular instance of that relationship).
      • Data is typically bulk-imported from external sources rather than entered interactively.
      • Neo4j has no built-in date type; dates are stored as strings.
    • Pattern Matching in Cypher

      Pattern matching syntax

      Cypher’s central operation is pattern matching. A MATCH clause describes a subgraph pattern, and the query engine finds all occurrences in the stored graph. Patterns use ASCII-art syntax to represent nodes and edges.

      Basic patterns

      PatternMeaning
      (a)-->(b)Any directed edge from a to b
      (a:Person)-->Any edge from a node of type Person
      (b:Movie)Any node of type Movie
      (a)-[:ACTED_IN]->(b)An ACTED_IN edge from a to b
      ()-->(a)<--()Nodes with two or more incoming edges
      (a)--(b)Edges in either direction (undirected match)

      The (a)-->(b) form is the simplest: it matches any node connected to any other node by a single directed edge. The (a:Person)-->(b:Movie) form adds type constraints.

      Variable-length paths (Kleene star)

      Cypher supports transitive matching using the Kleene star (zero or more steps):

      (a:Person)-[:ACTED_IN*]-(b)

      This matches paths of any length where every edge has type ACTED_IN. The * operator is analogous to the Kleene closure operation from formal languages: it matches zero, one, or more repetitions of the edge pattern.

      Bounded paths use the [*min..max] syntax:

      (a)-[*3..5]->(b)

      This matches paths of length 3 to 5 (inclusive). Useful bounds prevent runaway search on large graphs.

      Shortest path qualification:

      (a)-[:ACTED_IN*..6]-(b)

      This matches paths of length up to 6 (with *..6 as shorthand for *0..6).

      Friend-of-a-friend example

      MATCH (john {name: 'John'})-[:FRIEND]->()-[:FRIEND]->(fof)
      RETURN john.name, fof.name

      This query finds all nodes fof reachable from John by exactly two FRIEND edges. The empty () in the middle matches any intermediate node — it is a wildcard for the friend of John who is also the friend of fof.

      The result set contains one row per unique (john, fof) pair that is connected by a two-step path. If there are multiple mutual friends connecting John to the same fof, that fof appears only once (because MATCH returns distinct pattern matches, not every path).

      Co-actors query

      Actors who have appeared in the same film as each other:

      MATCH (p1:Person)-[:ACTED_IN]->(m:Movie)<-[:ACTED_IN]-(p2:Person)
      WHERE p1.person_id <> p2.person_id
      RETURN p1.name, p2.name, count(*) AS total
      ORDER BY total DESC
      LIMIT 10

      This pattern matches a pair of people both linked by ACTED_IN edges to the same movie node. The WHERE clause excludes self-pairs (the same person matched twice).

      Equivalent formulations of the same query

      The co-actors query can be written in several equivalent ways:

      Form 1 — Two separate patterns (conjunction):

      MATCH (p1:Person)-[:ACTED_IN]->(m:Movie),
            (p2:Person)-[:ACTED_IN]->(m:Movie)
      WHERE p1.person_id <> p2.person_id

      Form 2 — Chained pattern (as above):

      MATCH (p1:Person)-[:ACTED_IN]->(m:Movie)<-[:ACTED_IN]-(p2:Person)
      WHERE p1.person_id <> p2.person_id

      Form 3 — Two-hop transitive match:

      MATCH (p1:Person)-[:ACTED_IN*2]-(p2:Person)
      WHERE p1.person_id <> p2.person_id

      Form 3 is semantically different: it matches any two-step path where both edges are ACTED_IN, but it does not require that both edges point to the same movie. An actor could be connected to another actor by acting in Movie A and the second actor acting in Movie B — this would also be a two-hop path, but they are not co-stars. Form 3 overcounts.

      Return and aggregation

      RETURN can include aggregate functions (count, sum, avg, collect). When aggregation is present, non-aggregated columns form implicit grouping keys — the same semantics as SQL’s GROUP BY on all non-aggregated columns.

      Summary

      • Cypher patterns use (node)-[edge]->(node) ASCII-art syntax to describe subgraph structures.
      • Type constraints use colon syntax: (a:Person), [:ACTED_IN].
      • The Kleene star * enables transitive matching; bounds are written as [*min..max].
      • The same graph query can often be expressed in multiple ways with different semantics.
      • Two-hop paths via the same node require an intermediate variable, not just *2.
    • Graph Algorithms

      Built-in algorithm support

      Graph DBMSs provide efficient implementations of many classical graph algorithms. These are typically exposed as procedures (in Neo4j, via the Graph Data Science library) rather than formulated as Cypher queries. The DBMS can exploit adjacency-list storage and in-memory traversal to run algorithms that would be slow to express in a declarative query language.

      Taxonomy of algorithms

      CategoryAlgorithms
      TraversalBreadth-first search, depth-first search
      Path findingDijkstra’s shortest path, A*, all-pairs shortest paths
      CentralityPageRank, betweenness centrality, closeness centrality, eigenvector centrality
      Community detectionLouvain, label propagation, strongly connected components, weakly connected components
      SimilarityJaccard similarity, cosine similarity, node similarity
      Link predictionPreferential attachment, common neighbours, Adamic-Adar, resource allocation
      StructureSpanning trees, articulation points, bridges, cliques, triangle count, max flow, min cut

      Many of these algorithms overlap with content from the Algorithms course (Part IA).

      Graph metrics

      Community metrics

      MetricQuestion asked
      Edge/node ratioHow dense is the graph?
      DiameterWhat is the longest shortest path between any two nodes?
      Clustering coefficientHow likely are my friends to be friends with each other?
      Tree countHow many distinct spanning trees exist?
      ModularityHow well does a proposed partition reflect community structure?

      Centrality metrics

      Centrality measures answer: which nodes are most important to the structure of the graph? Different definitions of “important” give different rankings.

      MeasureDefinition of importance
      Degree centralityNumber of direct connections
      Betweenness centralityHow often a node lies on the shortest path between two other nodes
      Closeness centralityAverage shortest-path distance to all other nodes
      Eigenvector centralityImportance weighted by the importance of neighbours (PageRank is a variant)
      PageRankRandom-surfer model: probability that a random walker with teleportation is at the node

      Similarity metrics

      Similarity measures quantify how alike two nodes are based on their properties, their neighbourhood, or both. These are used for recommendation and deduplication.

      MeasureBasis
      Jaccard similarityOverlap of neighbour sets divided by union
      Cosine similarityAngle between property vectors
      Euclidean distanceStraight-line distance in property space

      Link prediction estimates the likelihood that a new edge will form between two currently unconnected nodes. It is used extensively in social networks to recommend friends.

      MethodHeuristic
      Preferential attachmentNodes with many connections gain more
      Common neighboursMore mutual friends = more likely to connect
      Adamic-AdarLike common neighbours but weights rare neighbours more heavily
      Resource allocationModels flow of resources through the network

      Applications

      Social networks

      Graph algorithms recommend new friend links, detect communities of shared interest, and identify influential users (influencers). A social graph with 1 billion nodes and hundreds of billions of edges requires approximate algorithms and distributed computation.

      Biological networks

      Metabolic networks with millions of nodes and edges represent biochemical reactions. Biologists query such graphs to find important structures for drug development — pathways that are critical for a pathogen but absent in the host. Community detection identifies groups of compounds that participate in the same biological process. Centrality metrics identify enzymes whose removal would cripple the organism.

      Road and logistics networks

      Shortest-path and minimum-spanning-tree algorithms underpin route planning in GPS navigation. Max-flow algorithms optimise transport and logistics schedules.

      Limitations of in-DBMS algorithms

      Graph algorithm libraries in DBMSs are convenient but not a substitute for understanding the algorithms themselves. The DBMS implementation may:

      • Use an approximation rather than an exact algorithm (common for centrality on large graphs).
      • Be limited by available memory.
      • Not expose the intermediate results needed for debugging or verification.

      For the Tripos, you should understand what the algorithms do, not merely that a DBMS provides them.

      Summary

      • Graph DBMSs offer built-in implementations of traversal, path-finding, centrality, community detection, similarity, and link-prediction algorithms.
      • Community metrics describe the macroscopic structure of the graph.
      • Centrality metrics rank nodes by how structurally important they are.
      • Link prediction estimates the probability of future edges, with applications in recommendation systems.
      • These algorithms have wide application in social networks, biology, logistics, and many other fields.
    • Graph DBMS Optimisations and Convergence

      Bacon numbers in Cypher

      The “Bacon number” problem (degrees of separation from Kevin Bacon) is a classic graph-traversal benchmark. In Cypher:

      MATCH paths = allShortestPaths(
        (m:Person {name: 'Kevin Bacon'})-[:ACTED_IN*]-(n:Person)
      )
      WHERE n.person_id <> m.person_id
      RETURN length(paths) / 2 AS bacon_number,
             COUNT(DISTINCT n.person_id) AS total
      ORDER BY bacon_number

      The allShortestPaths function finds every shortest path between Bacon and each other person. The edge count is divided by 2 because the path alternates between ACTED_IN edges and their inverse (a path from Person to Movie to Person to Movie to Person has 4 edges but represents a Bacon number of 2).

      This is dramatically simpler than the equivalent in SQL, which requires a recursive CTE with explicit cycle detection and shortest-distance tracking:

      WITH RECURSIVE BaconPath AS (
        SELECT p2.person_id, 1 AS bacon_number
        FROM acted_in a1
        JOIN acted_in a2 ON a1.movie_id = a2.movie_id
        WHERE a1.person_id = (SELECT person_id FROM people WHERE name = 'Kevin Bacon')
          AND a1.person_id <> a2.person_id
        UNION
        SELECT a4.person_id, bp.bacon_number + 1
        FROM BaconPath bp
        JOIN acted_in a3 ON bp.person_id = a3.person_id
        JOIN acted_in a4 ON a3.movie_id = a4.movie_id
        WHERE a3.person_id <> a4.person_id
          AND bp.bacon_number < 10
      )
      SELECT bacon_number, COUNT(*) AS total
      FROM BaconPath
      GROUP BY bacon_number
      ORDER BY bacon_number;

      The SQL version is longer, harder to read, and more prone to naive implementations causing infinite recursion.

      In-core optimisations: pointer-based edges

      Graph-oriented DBMSs that operate in-core (the entire graph fits in RAM) can use pointers to implement referential links between nodes. Following an edge becomes an O(1) pointer dereference:

      Node struct { type, properties, *edge_list }
      Edge struct { type, properties, *target_node, *next_edge }

      Traversal from a node to its neighbours is a linked-list walk (or array scan, if edges are stored in a contiguous block). In a relational system, the same operation requires an index lookup (B-tree or hash) on a foreign-key column, which is O(log n) or O(1) with hash but involves more indirection and cache-unfriendly access patterns.

      This pointer-based storage is the source of the graph database’s traversal-speed advantage. The cost is that pointer-based structures are harder to serialise to disk, harder to distribute across machines, and harder to make durable under crashes.

      Big-data implementations: Pregel

      For graphs too large to fit in memory, a different approach is needed. The Pregel model (Google, 2010) processes large graphs in a distributed, vertex-centric way:

      1. The graph is partitioned across many machines.
      2. Computation proceeds in supersteps. In each superstep, every vertex:
        • Receives messages sent to it in the previous superstep.
        • Updates its state.
        • Sends messages to other vertices (to be delivered in the next superstep).
      3. The computation ends when all vertices vote to halt and no messages are in flight.

      This is a bulk-synchronous parallel (BSP) model. Edges are streamed past processing elements. The system does not need random access to arbitrary parts of the graph — it processes neighbours in batches.

      The Pregel model is not unique to graph databases. Apache Giraph, Apache Spark’s GraphX, and other frameworks implement it as a graph processing layer over general-purpose distributed dataflow engines.

      Convergence of graph and relational systems

      The database landscape is converging rather than diverging. Key developments:

      DimensionGraph → RelationalRelational → Graph
      StorageGraph databases add columnar storage and compressionRDBMSs optimise in-core table sets (in-memory OLTP)
      Query languageCypher is being standardised as GQL (ISO/IEC 39075), complementing SQLSQL:2023 adds property graph queries (SQL/PGQ)
      IndexingGraph databases add B-tree and full-text indexes on propertiesRDBMSs add graph-specific indexes and recursive-CTE optimisation
      TransactionsNeo4j added ACID transactions early; others followedRDBMSs have had ACID for decades

      SQL/PGQ (Property Graph Queries) is part of the SQL:2023 standard. It allows graph-pattern matching within SQL, blurring the line between relational and graph query languages:

      SELECT p1.name, p2.name
      FROM GRAPH_TABLE (movie_graph
        MATCH (p1 IS Person)-[a IS ACTED_IN]->(m IS Movie)<-[b IS ACTED_IN]-(p2 IS Person)
        COLUMNS (p1.name, p2.name)
      );

      This is SQL with graph-pattern syntax. The convergence means that choosing a DBMS is less about the data model and more about the workload, the operational maturity, and the tooling ecosystem.

      Course summary

      The Databases course has covered multiple data models, each with strengths and weaknesses. Several themes recur:

      • Conceptual modelling matters. Having a conceptual model of data (E/R, relations, documents, graphs) is useful regardless of the implementation technology. Investment in data-model planning pays off well — the cost of fixing a bad schema later is high.

      • There is no universal database. No database system satisfies all possible requirements. Workloads differ in read/write ratio, query complexity, latency requirements, data volume, and consistency needs.

      • Staging is common practice. Staging between a principal storage model (used for updates) and optimised views, clones, or other alternatives for rapid querying is widely used. Materialised views, read replicas, and caching layers are standard architectural patterns.

      • Understand the trade-offs. It is best to understand the pros and cons of each approach and develop integrated solutions. A developer who knows only one database model is poorly equipped for real-world engineering.

      The current moment

      Today we see enormous churn and creative activity in the database field. The rise of AI has introduced vector databases, embedding-based retrieval, and natural-language-to-SQL translation. Graph databases compete with knowledge graphs and RDF triplestores for the same use cases. NewSQL systems claim to combine the scalability of NoSQL with the ACID guarantees of traditional RDBMSs. The field is not settling down — it is accelerating.

      Summary

      • The Bacon number query in Cypher is dramatically simpler than the equivalent recursive SQL.
      • In-core graph databases use pointers for O(1) edge traversal; relational systems use indexes.
      • The Pregel model processes large graphs in distributed supersteps with message passing.
      • The industry is converging: SQL:2023 adds graph-pattern queries; graph databases add relational features.
      • Conceptual data modelling is valuable regardless of the implementation.
      • No single database satisfies all requirements; understanding the trade-offs is essential.
  • Examinable Material and Tripos Revision

    Examinable syllabus checklist and full worked solutions for all available recent past paper questions (2021–2025)

    • Examinable Syllabus Checklist

      What the exam tests

      The Part IA Databases paper (Paper 3) covers 8 lectures taught in Michaelmas term. The exam typically has 2 questions, each worth 20 marks, testing a mixture of bookwork (definitions, terminology, concepts) and application (SQL queries, relational algebra, ER diagram construction, normalisation). Students must learn everything in the slide deck unless specifically marked unexaminable, must learn a core subset of SQL, and must know all words in the two course glossaries.

      Syllabus checklist

      1. Introduction (Lecture 1)

      • DBMS definition: software abstraction over secondary storage, narrow waist model
      • Fields, records, schema, key, index, query, transaction (glossary items)
      • Primary vs secondary storage; in-core vs persistent; big data
      • Consistency mechanisms: value range check, referential integrity, value atomicity, entity integrity
      • CRUD operations: Create, Read, Update, Delete
      • The three data models: relational, document, graph
      • Distributed databases: CAP theorem, BASE properties

      2. Entity-Relationship Models (Lecture 2)

      • Entities and attributes as the nouns of data modelling
      • Keys: natural keys, synthetic keys, primary keys; notation (underlining)
      • Relationships as verbs; cardinality notation (arrows for many-to-one, lines for many-to-many)
      • One-to-one, one-to-many, many-to-many cardinalities
      • Ternary relationships (diamonds connected to three entities)
      • Weak entities: definition, identifying relationship, discriminator, double-border notation
      • Entity hierarchies: IsA inheritance; specialisation and generalisation
      • Converting ER diagrams to schemas (later formalised in Lecture 4)

      3. The Relational Model (Lecture 3)

      • Mathematical relations: Cartesian product, subset definition, tuples
      • n-ary database relations; attribute domains
      • Relational algebra operators:
        • Selection (σ): horizontal subset; condition on attributes
        • Projection (π): vertical subset; eliminates duplicate rows (set semantics)
        • Union (∪): requires union-compatible schemas
        • Intersection (∩): rows common to both relations
        • Set difference (−): rows in first but not second
        • Cartesian product (×): all combinations
        • Renaming (ρ): rename attributes or the relation itself
        • Natural join (⋈): equijoin on all common attribute names, projected once
      • Relational algebra expressions: composition of operators into tree form

      4. ER to Relational Mapping (Lecture 4)

      • Update anomalies: insertion anomaly, deletion anomaly, update/modification anomaly
      • Motivation for splitting one big table into multiple smaller tables
      • Superkeys and keys: formal definitions; candidate keys; primary key selection
      • Foreign keys: definition, referencing constraint, referential integrity
      • SQL DDL: CREATE TABLE, PRIMARY KEY, REFERENCES, NOT NULL, data types
      • Implementing relationships of various cardinalities:
        • Many-to-many: junction/join table with two foreign keys
        • Many-to-one: foreign key on the many side
        • One-to-one: foreign key on either side with UNIQUE constraint
      • Implementing weak entities: composite key (owner’s key + discriminator); foreign key references the identifying owner
      • Implementing IsA hierarchies: three strategies (ER style, object-oriented, single table with nulls)

      5. Transactions and Normalisation (Lecture 5)

      • Transaction definition: a sequence of operations treated as a single logical unit
      • ACID properties:
        • Atomicity: all or nothing
        • Consistency: transactions preserve database invariants
        • Isolation: concurrent transactions do not interfere
        • Durability: committed changes survive failures
      • BASE properties: Basically Available, Soft state, Eventual consistency
      • ACID vs BASE: trade-off between consistency and availability/performance
      • Locks and throughput: read locks vs write locks; lock granularity; deadlock
      • Redundant data: causes and consequences (update anomalies, wasted storage, inconsistency)
      • Normalisation and normal forms:
        • First Normal Form (1NF): atomic values, no repeating groups
        • Second Normal Form (2NF): 1NF + no partial dependencies on a composite key
        • Third Normal Form (3NF): 2NF + no transitive dependencies
        • Boyce-Codd Normal Form (BCNF): stronger than 3NF; every determinant is a candidate key
      • OLAP vs OLTP: Online Analytical Processing vs Online Transaction Processing
      • ETL: Extract, Transform, Load (for data warehousing)
      • Read-oriented vs write-oriented databases; denormalisation trade-offs

      6. Document-Oriented Databases (Lecture 6)

      • Semi-structured data: data that is self-describing (schema embedded with data)
      • XML: structure, elements, attributes, well-formed vs valid, DTD, XSD
      • JSON: objects, arrays, primitive types; comparison with XML
      • Key-value stores: simplest document model; get/put/delete interface
      • Document-oriented and aggregate-oriented databases
      • NoSQL movement: motivations (web scale, flexible schema, horizontal scaling)
      • TinyDB: a document database example; query-by-example
      • Schema-free ideal: benefits (flexibility, rapid iteration) and critiques (no enforced constraints, application must validate)
      • Branded types: user-defined type identifiers embedded in documents; inverse of semi-structured data

      7. Further SQL (Lecture 7)

      • Join complexity: nested-loop join (O(NM)), hash join, merge join, index-assisted join
      • Database indexes: B-trees, hash indexes; how they reduce join cost
      • Multisets (bags) vs sets: SQL defaults to bags; DISTINCT keyword; how this differs from relational algebra’s set semantics
      • GROUP BY and aggregate functions:
        • GROUP BY creates groups of rows sharing a common value
        • Aggregate functions: MIN, MAX, SUM, AVG, COUNT
        • HAVING clause: filters groups after aggregation (WHERE filters before)
        • Aggregates must be commutative and associative
      • NULL and three-valued logic:
        • NULL means unknown, not zero and not empty string
        • Three-valued truth tables: TRUE, FALSE, UNKNOWN
        • NULL behaviour in comparisons, arithmetic, and aggregates
        • IS NULL / IS NOT NULL tests
        • Outer joins: LEFT, RIGHT, FULL; preserving unmatched rows with NULL padding
      • Function and relation composition: applying one query’s result as input to another
      • Transitive closure: why it cannot be expressed in standard relational algebra
      • Recursive SQL: WITH RECURSIVE construct
        • Base case (non-recursive union member)
        • Recursive case (union member referencing the CTE name)
        • Termination condition (empty recursive result)
        • Applications: bill of materials, graph reachability, Bacon numbers

      8. Graph Databases (Lecture 8)

      • Graph database model: typed nodes and typed directed edges; properties on both
      • Why relational stores are inefficient for graph traversal: many joins required
      • Neo4j: native graph database; property graph model
      • Cypher query language:
        • ASCII art pattern matching: (n:Node)-[:REL]->(m)
        • MATCH, WHERE, RETURN clauses
        • Variable-length paths: -[*1..5]->
        • CREATE, MERGE for data manipulation
      • Pattern matching: declarative specification of graph patterns
      • Graph algorithms: BFS, DFS, shortest path, PageRank
      • Convergence: graph features appearing in relational DBs (recursive CTEs, JSON columns); relational features in graph DBs (aggregation, ACID transactions)

      Exam technique

      1. Bookwork definitions (4-6 marks): State the definition concisely. Give the context. 3-4 sentences is enough. If asked to compare two things, use a table or structured contrast.
      2. SQL / RA application (6-10 marks): Write the query carefully. Think about duplicates, NULLs, edge cases. If the question gives a schema, reference the column names precisely. For RA, use standard notation (σ, π, ⋈, etc.).
      3. ER diagram construction (6-8 marks): Read the scenario carefully. Identify entities (nouns), relationships (verbs), and attributes. Decide cardinality. Check for weak entities and IsA hierarchies. Underline keys.
      4. Extension / discussion (6-8 marks): Open-ended questions about trade-offs, design decisions, or evaluating a proposed solution. Two well-developed points with specific examples are better than four vague ones.

      Key concepts to memorise

      ConceptOne-line summary
      Natural joinEquijoin on all common attribute names, projecting them once
      Foreign keyAttribute(s) in one relation referencing the primary key of another
      Weak entityEntity identified by its relationship to an owner entity plus a discriminator
      ACIDAtomicity, Consistency, Isolation, Durability
      BASEBasically Available, Soft state, Eventual consistency
      CAP theoremA distributed system can satisfy at most two of: Consistency, Availability, Partition tolerance
      CAP: ConsistencyAll nodes see the same data at the same time
      CAP: AvailabilityEvery request receives a (non-error) response
      CAP: Partition toleranceSystem continues despite network partitions
      3NFNo transitive dependencies on non-prime attributes
      BCNFEvery determinant is a candidate key
      GROUP BYPartition rows into groups sharing a common value; aggregates reduce each group
      Outer joinPreserves unmatched rows from one or both sides, padding with NULL
      Recursive CTEWITH RECURSIVE: base union + recursive step; terminates when no new rows
      Transitive closureCannot be expressed in standard relational algebra
    • Tripos Worked Solutions: 2021 Paper 3 Question 2

      Question: Databases (Dr Timothy Griffin) --- 20 marks

      The question uses the relational movie database with tables: movies(movie_id, title, year), people(person_id, name), has_position(person_id, movie_id, position), plays_role(person_id, movie_id, role), genres(genre_id, genre), has_genre(movie_id, genre_id).

      (a) Interpreting an SQL count [6 marks]

      Query:

      SELECT COUNT(*) FROM has_position WHERE position = 'director';

      Result: 1422. Conclusion: “Our database contains information on 1422 directors.”

      Is the conclusion justified?

      Answer:

      No, the conclusion is not justified. The query counts the number of entries (rows) in has_position where position = 'director', not the number of distinct directors. A person who directs multiple movies will appear in has_position multiple times, each counting separately. For example, if Christopher Nolan has directed 10 movies, he contributes 10 to the count.

      To obtain the number of distinct directors, the correct query is:

      SELECT COUNT(DISTINCT person_id)
      FROM has_position
      WHERE position = 'director';

      A further subtlety: the original query does not verify that the person_id values actually exist in the people table. If referential integrity is not enforced, has_position could contain person_id values with no matching row in people. A robust query would join to people:

      SELECT COUNT(DISTINCT hp.person_id)
      FROM has_position hp
      JOIN people p ON p.person_id = hp.person_id
      WHERE hp.position = 'director';

      However, the primary flaw is the failure to use DISTINCT. The count of entries is an upper bound on the count of directors; it is correct only if every director directed exactly one film.

      (b) Missing information from filtered query [6 marks]

      Query:

      SELECT person_id, name, position, COUNT(*) AS total
      FROM has_position AS hp
      JOIN people AS p ON p.person_id = hp.person_id
      WHERE position <> 'actor' AND name = 'Stan Lee'
      GROUP BY person_id, name, position;

      Result: (Stan Lee, writer, 15). Conclusion: “Stan Lee did not produce any movies.”

      Is the conclusion justified?

      Answer:

      No, the conclusion is not justified. The WHERE clause explicitly excludes rows where position = 'actor', but the query only returns rows grouped by position. The presence of (Stan Lee, writer, 15) tells us Stan Lee has 15 entries as a writer. It says nothing about whether he also appears as a producer, because the GROUP BY groups by position, and the query would return a separate row for each position Stan Lee holds (provided that position is not ‘actor’).

      To check whether Stan Lee was a producer, one should query for all positions held:

      SELECT DISTINCT position
      FROM has_position hp
      JOIN people p ON p.person_id = hp.person_id
      WHERE p.name = 'Stan Lee';

      If this returns both 'writer' and 'producer', then Stan Lee did produce movies. The original query’s GROUP BY person_id, name, position means that if Stan Lee had both writer and producer roles, the result would have shown two rows (one for each position). The fact that only one row appears does suggest he may only hold the writer role in this database, but the conclusion that he “did not produce any movies” cannot be drawn with certainty without examining the complete data. The query omits ‘actor’ roles, and the absence of a producer row could also be due to data entry errors or the fact that the specific position name might be stored differently (e.g., 'producer' vs 'executive producer').

      The correct approach: query without the position <> 'actor' filter, or specifically check for the producer role:

      SELECT COUNT(*)
      FROM has_position hp
      JOIN people p ON p.person_id = hp.person_id
      WHERE p.name = 'Stan Lee' AND hp.position = 'producer';

      If this returns 0, only then can we conclude Stan Lee did not produce any movies (according to this database).

      (c) Cross join misinterpretation [8 marks]

      Query:

      SELECT r1.role, m1.year, COUNT(*) AS total
      FROM plays_role AS r1, plays_role AS r2, movies AS m1, movies AS m2, people AS p
      WHERE p.person_id = r1.person_id
        AND r1.person_id = r2.person_id
        AND r1.role = r2.role
        AND r1.movie_id = m1.movie_id
        AND r2.movie_id = m2.movie_id
        AND p.name = 'Jennifer Lawrence';
      GROUP BY r1.role, m1.year;

      Result includes: (Katniss Everdeen, 2012, 4). Conclusion: “Jennifer Lawrence played Katniss Everdeen in 4 movies released in 2012.”

      Is the conclusion justified?

      Answer:

      No, the conclusion is not justified. The query uses a cross join pattern (comma-separated table list in the FROM clause creates a Cartesian product) that pairs every role with every other role where the same person plays the same character. The COUNT(*) counts the number of (r1, r2) pairs, not the number of distinct (role, year, movie) triples.

      To see why the result is 4, suppose Jennifer Lawrence played Katniss Everdeen in exactly 2 movies released in 2012. The cross join self-pairing produces:

      • r1 = movie A, r2 = movie A (1 pair)
      • r1 = movie A, r2 = movie B (1 pair)
      • r1 = movie B, r2 = movie A (1 pair)
      • r1 = movie B, r2 = movie B (1 pair)

      Total: 4 pairs. But there are only 2 distinct movies. The query counts combinations, not distinct instances.

      Worse, the GROUP BY r1.role, m1.year groups by the role name and the year of m1 (the movie joined to r1). This means all four pairs above fall into the same group (same role, same year), giving total = 4. The year from m2 is not visible in the result.

      The correct query to count distinct movies where Jennifer Lawrence played a given role in a given year:

      SELECT role, year, COUNT(DISTINCT r.movie_id) AS total
      FROM plays_role AS r
      JOIN movies AS m ON m.movie_id = r.movie_id
      JOIN people AS p ON p.person_id = r.person_id
      WHERE p.name = 'Jennifer Lawrence'
      GROUP BY role, year;

      This counts each movie exactly once per (role, year) group. The COUNT(DISTINCT r.movie_id) ensures duplicates from the join are eliminated.

      A more general lesson: when a query contains a self-join or cross join, always ask whether the count is counting the thing you think it is counting, or counting pairs/combinations. The COUNT(*) form is particularly prone to this misinterpretation.

    • Tripos Worked Solutions: 2022 Paper 3 Questions 1 and 2

      Question: Databases (Dr Timothy Griffin) --- 2 questions, 10 marks each

      Uses the relational movie database: movies(movie_id, title, year), people(person_id, name), has_position(person_id, movie_id, position), plays_role(person_id, movie_id, role), genres(genre_id, genre), has_genre(movie_id, genre_id).

      Question 1: SQL queries on the movie database [10 marks]

      (a) Number of romantic comedies [4 marks]

      Write an SQL query to return the number of movies that are romantic comedies (i.e., movies that have both the ‘Romance’ and ‘Comedy’ genres).

      Answer:

      A movie is a romantic comedy if it has (at least) one has_genre entry linking it to the ‘Romance’ genre and (at least) one has_genre entry linking it to the ‘Comedy’ genre. We join has_genre to itself on movie_id and join each to genres to filter by genre name:

      SELECT COUNT(DISTINCT hg1.movie_id)
      FROM has_genre AS hg1
      JOIN genres AS g1 ON g1.genre_id = hg1.genre_id
      JOIN has_genre AS hg2 ON hg2.movie_id = hg1.movie_id
      JOIN genres AS g2 ON g2.genre_id = hg2.genre_id
      WHERE g1.genre = 'Romance' AND g2.genre = 'Comedy';

      The DISTINCT is a safeguard: if a movie has multiple ‘Romance’ or ‘Comedy’ genre entries (e.g., sub-genres), the self-join could produce multiple matching rows for the same movie, inflating the count.

      An alternative using subqueries:

      SELECT COUNT(*)
      FROM movies
      WHERE movie_id IN (
          SELECT movie_id FROM has_genre JOIN genres ON genres.genre_id = has_genre.genre_id
          WHERE genre = 'Romance'
      )
      AND movie_id IN (
          SELECT movie_id FROM has_genre JOIN genres ON genres.genre_id = has_genre.genre_id
          WHERE genre = 'Comedy'
      );

      Both approaches are correct. The subquery approach is arguably clearer but the self-join approach (or using INTERSECT) may perform differently depending on the query planner.

      (b) Complete SQL for co-actors in romantic comedies [3 marks]

      Complete the SQL query to find pairs of co-actors (people who have acted in the same romantic comedy):

      SELECT R1.person_id AS pid1, R2.person_id AS pid2, M.movie_id AS movie_id
      FROM plays_role AS R1
      JOIN plays_role AS R2 ON R1.movie_id = R2.movie_id
      JOIN movies AS M ON M.movie_id = R1.movie_id
      WHERE R1.person_id <> R2.person_id
      AND R1.movie_id IN (
          -- subquery from part (a) to select romantic comedy movie_ids
          SELECT hg1.movie_id
          FROM has_genre hg1
          JOIN genres g1 ON g1.genre_id = hg1.genre_id
          JOIN has_genre hg2 ON hg2.movie_id = hg1.movie_id
          JOIN genres g2 ON g2.genre_id = hg2.genre_id
          WHERE g1.genre = 'Romance' AND g2.genre = 'Comedy'
      );

      Key points:

      • The JOIN ... ON R1.movie_id = R2.movie_id pairs every actor in a given movie with every other actor in the same movie.
      • R1.person_id <> R2.person_id prevents a person from being their own co-actor and eliminates duplicate self-pairings.
      • The subquery restricts results to romantic comedies only.
      • The result contains both (pid1=A, pid2=B) and (pid1=B, pid2=A) for each co-actor pair. If symmetric pairs are not desired, use R1.person_id < R2.person_id instead of <>.

      (c) Co-actor chains [3 marks]

      Write an SQL query to find actors who have a co-actor chain of length 2 to a given actor (i.e., A co-acted with B who co-acted with C). Use the co-actor view from part (b).

      Answer:

      We first define a view (or CTE) for the co-actor relation from part (b), then self-join it to find length-2 chains:

      WITH co_actors AS (
          SELECT R1.person_id AS pid1, R2.person_id AS pid2
          FROM plays_role AS R1
          JOIN plays_role AS R2 ON R1.movie_id = R2.movie_id
          WHERE R1.person_id <> R2.person_id
      )
      SELECT DISTINCT ca1.pid1 AS actor, ca2.pid2 AS chain_end
      FROM co_actors ca1
      JOIN co_actors ca2 ON ca1.pid2 = ca2.pid1
      WHERE ca1.pid1 = ?  -- given actor's person_id
        AND ca2.pid2 <> ca1.pid1;  -- exclude the original actor themselves

      For chains of arbitrary length (transitive closure), a recursive CTE is required:

      WITH RECURSIVE co_actor_chain(n, person_id) AS (
          -- Base case: the given actor at distance 0
          SELECT 0, person_id FROM people WHERE name = 'Given Name'
      
          UNION
      
          -- Recursive case: extend chain by one co-actor step
          SELECT c.n + 1, ca.pid2
          FROM co_actor_chain c
          JOIN co_actors ca ON ca.pid1 = c.person_id
          WHERE c.n < 6  -- depth limit to avoid infinite loops
      )
      SELECT DISTINCT person_id, n
      FROM co_actor_chain
      ORDER BY n;

      The WHERE c.n < 6 limits recursion depth. Without a limit, cycles in the co-actor graph could cause infinite recursion. This is the Bacon number problem: finding the distance between any two actors in the collaboration graph.


      Question 2: ER modelling for supervision allocation [10 marks]

      The scenario concerns allocating students to supervision groups for courses at Cambridge. Each course has multiple supervision groups, each run by a supervisor. Students are allocated to groups.

      (a) Argue that model M2 is better than M1 [4 marks]

      Two ER models M1 and M2 are shown (not reproduced here). M1 links Students directly to Supervisors. M2 introduces a Group entity and an Allocation relationship. Argue why M2 is the better model.

      Answer:

      M2 is better than M1 for several reasons:

      1. M1 conflates distinct concepts. M1 models the relationship “Student is supervised by Supervisor” directly, but this misses the fact that supervision happens in groups for specific courses. A student may be in multiple supervision groups (one for each course), and a supervisor may run different groups for different courses. M1’s direct Student-Supervisor link cannot distinguish which course a supervision belongs to, nor can it represent multiple students in the same group.

      2. M2 properly captures the ternary nature. The real-world relationship is ternary: Student-Course-Group-Supervisor. M2 breaks this down cleanly: Group is an entity associated with a Course and a Supervisor; Allocation links Student to Group. This separation of concerns means:

      • A supervisor can run multiple groups for different courses.
      • A student can be in different groups for different courses.
      • The group identity persists independently of its members.

      3. M2 avoids redundancy. In M1, if a supervisor changes, every student linked to that supervisor must be updated. In M2, only the Group-Supervisor relationship changes once. Similarly, adding a student to a group does not require storing the supervisor and course information repeatedly.

      4. M2 supports queries about groups as first-class entities. With M2, one can ask “how many students are in group X?” or “which groups does course Y have?” without denormalising data. In M1, answering “which students are in the same supervision group?” requires an implicit grouping that the schema does not represent.

      (b) Implement M2 in a relational schema [3 marks]

      Write the CREATE TABLE statements for M2, including primary keys and foreign keys.

      Answer:

      CREATE TABLE Courses (
          course_id    INTEGER PRIMARY KEY,
          course_name  VARCHAR(100) NOT NULL
      );
      
      CREATE TABLE Supervisors (
          supervisor_id  INTEGER PRIMARY KEY,
          name           VARCHAR(100) NOT NULL,
          email          VARCHAR(100)
      );
      
      CREATE TABLE Students (
          crsid       VARCHAR(10) PRIMARY KEY,
          name        VARCHAR(100) NOT NULL,
          college     VARCHAR(50)
      );
      
      CREATE TABLE Groups (
          group_id       INTEGER PRIMARY KEY,
          course_id      INTEGER NOT NULL,
          supervisor_id  INTEGER NOT NULL,
          group_name     VARCHAR(50),
          FOREIGN KEY (course_id) REFERENCES Courses(course_id),
          FOREIGN KEY (supervisor_id) REFERENCES Supervisors(supervisor_id)
      );
      
      CREATE TABLE GroupMembers (
          group_id  INTEGER NOT NULL,
          crsid     VARCHAR(10) NOT NULL,
          PRIMARY KEY (group_id, crsid),
          FOREIGN KEY (group_id) REFERENCES Groups(group_id),
          FOREIGN KEY (crsid) REFERENCES Students(crsid)
      );

      Cardinality analysis:

      • Supervisor to Group: 1:N (one supervisor may run many groups; each group has exactly one supervisor, via Groups.supervisor_id).
      • Student to GroupMember: 1:N (one student can be in many groups for different courses; each GroupMembers row references one student).
      • Course to Group: 1:N (one course may have many groups; each group belongs to one course).
      • Group to GroupMember: 1:N (one group has many student members).

      The composite primary key (group_id, crsid) on GroupMembers ensures a student cannot be in the same group twice.

      (c) SQL for students and their groups [3 marks]

      Write an SQL query listing all students with their course and group, including students who are not in any group.

      Answer:

      The requirement to include students with no group mandates a LEFT JOIN:

      SELECT s.crsid, s.name, g.course_id, g.group_id
      FROM Students s
      LEFT JOIN GroupMembers gm ON s.crsid = gm.crsid
      LEFT JOIN Groups g ON gm.group_id = g.group_id;

      With LEFT JOIN, every row from Students is preserved. If a student has no matching GroupMembers row (and hence no matching Groups row), course_id and group_id appear as NULL. An INNER JOIN would silently omit these students.

      An alternative using UNION:

      SELECT s.crsid, s.name, g.course_id, g.group_id
      FROM Students s
      INNER JOIN GroupMembers gm ON s.crsid = gm.crsid
      INNER JOIN Groups g ON gm.group_id = g.group_id
      UNION
      SELECT s.crsid, s.name, NULL, NULL
      FROM Students s
      WHERE s.crsid NOT IN (SELECT crsid FROM GroupMembers);

      But the LEFT JOIN version is cleaner and typically more efficient, since it scans the data once rather than twice.

    • Tripos Worked Solutions: 2023 Paper 3 Questions 1 and 2

      Question: Databases (Dr Timothy Griffin) --- 2 questions, 10 marks each

      Question 1 [10 marks]

      (a) Vehicle manufacturer ER diagram and schema [4 marks]

      (i) Draw an E/R diagram for a vehicle manufacturer.

      The scenario: a manufacturer produces vehicles; each vehicle has a VIN, model, number of seats, and colour. Each vehicle contains one engine; each engine has an engine ID, fuel type, and horsepower. Engines are supplied by suppliers; each supplier has a supplier ID, name, and contact details. A supplier may supply many engines, but each engine comes from exactly one supplier.

      Answer:

      The ER diagram has three entities and two relationships:

      Vehicle-Engine-Supplier ER Diagram: Vehicle has a 1-to-1 relationship with Engine, which in turn has an N-to-1 supplies relationship with Supplier

      • has_engine: Vehicle to Engine, 1:1 (each vehicle has one engine; each engine is in one vehicle).
      • supplies: Supplier to Engine, 1:N (a supplier can supply many engines; each engine has exactly one supplier). The arrow from Engine to Supplier indicates the “many” side pointing to the “one” side.

      (ii) Draw the relational schema with keys underlined.

      Answer:

      Vehicles(vin, model, seats, colour)
               ___
      
      Engines(engine_id, fuel, horsepower)
              _________
      
      Supplies(supplier_id, engine_id)
               ___________  _________
      
      Suppliers(supplier_id, name, contact)
                ___________

      Since has_engine is 1:1, we can absorb the relationship by putting a foreign key in either table. Here we assume engine_id is included in Vehicles (not shown explicitly but the Supplies table joins Suppliers to Engines).

      A more complete representation showing the foreign keys:

      Vehicles(vin, model, seats, colour, engine_id)
               ___                          _________
               PK                           FK → Engines
      
      Engines(engine_id, fuel, horsepower, supplier_id)
              _________                    ___________
              PK                           FK → Suppliers
      
      Suppliers(supplier_id, name, contact)
                ___________
                PK

      The has_engine relationship is implemented by Vehicles.engine_id referencing Engines.engine_id (with either side being the FK since it is 1:1). The supplies relationship is implemented by Engines.supplier_id referencing Suppliers.supplier_id (FK on the many side).

      (iii) Foreign keys and referential integrity.

      In this schema:

      • Vehicles.engine_id is a foreign key referencing Engines.engine_id.
      • Engines.supplier_id is a foreign key referencing Suppliers.supplier_id.

      The schema satisfies referential integrity for a given instance if:

      • Every engine_id value in Vehicles exists in Engines.
      • Every supplier_id value in Engines exists in Suppliers.

      Whether a particular example instance satisfies this depends on the data. If the instance has no orphaned foreign key values, it satisfies referential integrity.

      (b) GROUP BY and reduction operators [2 marks]

      (i) Why does GROUP BY require a reduction operator?

      Answer:

      GROUP BY partitions the rows of a table into groups where all rows in a group share the same value for the grouping column(s). After grouping, there are multiple rows in each group, but the output must contain exactly one row per group. A reduction (aggregate) operator collapses all the values in the group into a single scalar value. Without an aggregate function, it would be ambiguous which row’s value to emit for non-grouped columns.

      For example, in SELECT department, AVG(salary) FROM employees GROUP BY department, the AVG reduces the many salaries per department into one average per department.

      (ii) What properties should reduction operators satisfy and why?

      Answer:

      Reduction operators should be commutative and associative. These properties ensure the result is independent of the order in which rows are processed. Since SQL does not guarantee any particular order of rows within a group (unless ORDER BY is specified in an aggregate context, which is not standard), the aggregate must produce the same result regardless of row ordering.

      • Commutative: f(a, b) = f(b, a). The result does not depend on which row is processed first.
      • Associative: f(f(a, b), c) = f(a, f(b, c)). The result does not depend on how rows are paired during parallel aggregation.

      MIN, MAX, SUM, and COUNT satisfy both properties. AVG is not directly associative, but it can be decomposed into SUM and COUNT which are, and then SUM / COUNT gives the average. This decomposition matters for distributed query processing where partial aggregates from different nodes must be combined.

      (c) Eventual consistency [2 marks]

      Define eventual consistency. State one advantage and one disadvantage.

      Answer:

      Definition: Eventual consistency is a consistency model in which, if no new updates are made to a data item, eventually all accesses to that item will return the last updated value. During the period between an update and the convergence of all replicas, different replicas may return different (stale) values.

      Advantage: Higher availability. A system using eventual consistency can always accept reads and writes, even during network partitions or replica failures. Any replica can serve a read request without coordinating with others, reducing latency for geographically distributed users.

      Disadvantage: During the convergence window, clients may observe inconsistent states. An application reading from two different replicas could see contradictory data, leading to incorrect decisions. For example, after a bank transfer, one replica might still show the money in the source account while another shows it deducted --- an application summing both replicas could double-count the amount.

      (d) Graph database architecture [2 marks]

      Why does a graph database hold just one graph?

      Answer:

      A graph database holds a single graph because the model is based on typed nodes and typed directed arcs, where all entities and all relationships coexist in one large connected structure. The value of a graph database lies in path-oriented queries that traverse arbitrary chains of relationships across different entity types (e.g., “find all suppliers within 3 hops of a given part through any combination of relationships”). Splitting the data into multiple disconnected graphs would break these multi-hop queries that cross entity type boundaries. The entire database is the graph; individual “tables” or collections are implemented as node labels and relationship types within the single graph.


      Question 2 [10 marks]

      (a) Join associativity and execution plans [2 marks]

      Natural join is associative. Yet different association orders (join plans) can give significantly different execution times. Explain, with an example.

      Answer:

      Natural join is indeed associative: (RS)T=R(ST)(R \bowtie S) \bowtie T = R \bowtie (S \bowtie T). However, associativity is an algebraic identity concerning the result, not the cost of computing it.

      Consider: R(A,B)R(A, B) with 10 rows, S(B,C)S(B, C) with 1000 rows, T(C,D)T(C, D) with 10 rows.

      Plan 1: (RS)T(R \bowtie S) \bowtie T

      • First compute RSR \bowtie S. If all 10 values of B in R match all 1000 values in S, the intermediate result has up to 10,000 rows. Then join this with T (10 rows), producing the final result.
      • The large intermediate result (10,000 rows) must be materialised and processed by the second join, consuming memory and I/O.

      Plan 2: R(ST)R \bowtie (S \bowtie T)

      • First compute STS \bowtie T. If all 10 values of C in T match all 1000 values in S, the intermediate result is again up to 10,000 rows. Then join with R.
      • Same worst case, but with different actual data distributions, one plan may be much cheaper.

      The optimal plan depends on:

      • Selectivity of the joins (how many rows actually match).
      • Index availability (if an index on the join column exists, the cost drops from O(NM) to O(N log M)).
      • Data statistics collected by the DBMS (histograms, cardinalities).

      The query planner uses these statistics to estimate the cost of each plan and choose the cheapest. Two algebraically equivalent expressions can have orders-of-magnitude differences in execution time.

      (b) Keys and primary key selection [2 marks]

      Give an example of a relation with multiple potential keys. Discuss reasons why one might not be suitable as the primary key.

      Answer:

      Consider a People relation:

      People(national_insurance_no, email, name, date_of_birth)

      Both national_insurance_no and email are candidate keys (assuming no two people share an email).

      Reasons either might be unsuitable as the primary key:

      National Insurance number:

      • Not everyone has one (e.g., international students, visitors). A primary key value cannot be NULL, so people without a NI number cannot be stored.
      • NI numbers can, in rare circumstances, be reassigned after a person’s death.
      • Privacy concerns: using a government-issued identifier as a database key may be undesirable for data-protection reasons.

      Email address:

      • Email addresses can change over time. Updating a primary key value cascades to all foreign key references, which is expensive and error-prone.
      • Email addresses can be shared (e.g., family email accounts), violating uniqueness.
      • An email address may be NULL (not everyone has one), but primary key columns must be NOT NULL.

      The preferred approach is often a synthetic key (e.g., an auto-incrementing integer person_id), which has no external meaning and therefore never needs to change. The natural keys (national_insurance_no, email) can still have UNIQUE constraints to enforce their logical uniqueness.

      (c) XML and semi-structured data [3 marks]

      (i) When is XML stored with varying structure useful?

      Answer:

      XML with varying (flexible) structure is useful in several scenarios:

      1. Heterogeneous data sources. When integrating data from different systems, each system may produce differently structured XML for the same conceptual entity. A rigid schema would reject data from some sources; a flexible schema accommodates all of them.

      2. Evolving schemas. When the data format changes over time (new fields added, old fields deprecated), existing documents should not become invalid. A flexible structure allows old and new documents to coexist without requiring a migration of all historical data.

      3. Optional or sparse data. When different instances of an entity have very different sets of attributes, a fixed schema would require many nullable columns. A flexible XML structure stores only the attributes that are present.

      (ii) How would you export relational data to a single XML document?

      Answer:

      For each table, create a repeated element at the root. For each row, create a child element. For each column, create either a named child element or an attribute.

      Example for the movie database:

      <database>
        <movies>
          <movie>
            <movie_id>tt0111161</movie_id>
            <title>The Shawshank Redemption</title>
            <year>1994</year>
          </movie>
          <movie>
            <movie_id>tt0068646</movie_id>
            <title>The Godfather</title>
            <year>1972</year>
          </movie>
        </movies>
        <people>
          <person>
            <person_id>1</person_id>
            <name>Tim Robbins</name>
          </person>
        </people>
        <has_genre>
          <entry>
            <movie_id>tt0111161</movie_id>
            <genre_id>1</genre_id>
          </entry>
        </has_genre>
      </database>

      The root element <database> contains one child per table. Each table element contains one child per row. Each row element contains one child per column (or attributes for columns). The same structure works for JSON: a top-level object with keys for each table, each containing an array of row objects.

      (d) Projection in different data models [3 marks]

      Projection in relational algebra selects a subset of columns. What is the equivalent in a document database and in a graph database?

      Answer:

      Document database: There is no direct equivalent of projection because documents do not have a fixed column set. Each document is a self-describing collection of key-value pairs (possibly nested). The closest operation is field selection: specifying which keys to include or exclude in the query result. For example, in MongoDB: db.collection.find({}, {title: 1, year: 1, _id: 0}) returns only the title and year fields. However, this differs from relational projection because:

      • Different documents in the same collection may have different sets of fields, so the “projection” is per-document rather than schema-wide.
      • Nested fields can be individually projected (e.g., {"address.city": 1}).

      Graph database: Again, no direct equivalent. Nodes and edges have property maps (key-value dictionaries). Projection is achieved by specifying which properties to return in the RETURN clause of a Cypher query. For example:

      MATCH (p:Person)
      RETURN p.name, p.birth_year

      This returns only the name and birth_year properties from each matched Person node. Properties not listed in RETURN are omitted from the result. Unlike relational projection, this operates on named properties within a flexible property map, not on a fixed column position.

    • Tripos Worked Solutions: 2024 Paper 3 Questions 1 and 2

      Question: Databases (Dr Timothy Griffin) --- 2 questions, 10 marks each

      Question 1 [10 marks]

      (a) Relational union and intersection [2 marks]

      (i) What is required for two relations to be union-compatible?

      Answer:

      Two relations are union-compatible if they have the same arity (number of attributes) and the corresponding attributes share the same domain (data type). For example, R(A: INTEGER, B: VARCHAR) and S(C: INTEGER, D: VARCHAR) are union-compatible: both have two attributes where the first is integer and the second is varchar. The attribute names do not need to match; the result takes the attribute names from the first operand.

      (ii) Does intersection require the same condition?

      Answer:

      Yes, intersection requires union-compatibility for the same reason: intersection is defined as the set of tuples that appear in both relations, and comparing tuples only makes sense if they have the same structure. The formal definition is RS={ttRtS}R \cap S = \{t \mid t \in R \land t \in S\}, which requires that the tuples in R and S be comparable.

      (b) Cardinality bounds [2 marks]

      Relation R(P, B) has P rows, relation S(B, Q) has Q rows. Consider R ∪ S (union) and R ⋈ S (natural join). Give the minimum and maximum number of rows possible.

      Answer:

      Union R ∪ S: R and S must be union-compatible for R ∪ S to be defined. Assuming they share schema (A, B):

      • Minimum: max(P,Q)\max(P, Q). Occurs when one relation is a subset of the other; the union is just the larger relation.
      • Maximum: P+QP + Q. Occurs when R and S are disjoint (no tuple appears in both).

      Natural join R ⋈ S: Joins on the common attribute B. R has schema (P, B), S has schema (B, Q). The join is R ⋈ S with result schema (P, B, Q).

      • Minimum: 0. Occurs when no value of B appears in both R and S (the sets of B values are disjoint).
      • Maximum: P×QP \times Q. Occurs when all rows in R and S share the same value of B. Every row in R matches every row in S, producing the Cartesian product. If the common B value appears PP times in R and QQ times in S, all P×QP \times Q pairs are produced.

      (c) Textbook chapters ER diagram [3 marks]

      A textbook database stores books and their chapters. Each book has an ISBN, title, and author. Each chapter has a number, title, and page count. Chapters belong to exactly one book. A book can have many chapters.

      (i) Draw the ER diagram.

      Answer:

      Book-Chapter Weak Entity ER Diagram: Book is a strong entity containing Chapter weak entities (double bordered) via the Contains identifying relationship (double diamond)

      • Book is a strong entity with primary key ISBN.
      • Chapter is a weak entity: a chapter is identified by its number within a specific book (the same chapter number can appear in different books). The relationship Contains is the identifying relationship.
      • Cardinality: 1:N (one book contains many chapters; each chapter belongs to exactly one book).

      (ii) Write the relational schema.

      Answer:

      Books(ISBN, title, author)
            ____
      
      Chapters(ISBN, chapter_number, title, page_count)
               ____  ______________
      • Books: primary key is ISBN (simple key, single attribute).
      • Chapters: primary key is (ISBN, chapter_number) (composite key). ISBN is also a foreign key referencing Books.ISBN.
      • The composite key ensures chapter numbers are unique within a book but can be reused across different books.

      (d) Extending the textbook database [3 marks]

      The database might be extended to support full-text search and cross-referencing. Discuss what structural changes are needed and the benefits of enforcing consistency rules.

      Answer:

      Structural changes for full-text search:

      • Add a content column (type TEXT or CLOB) to the Chapters table to store the full text of each chapter.
      • Create a full-text index on the content column. Many RDBMSs provide specialised full-text index types (e.g., PostgreSQL’s GIN index with tsvector, MySQL’s FULLTEXT index) that support stemming, ranking, and phrase search.
      • Alternatively, store chapter text in a separate ChapterText(chapter_id, paragraph_number, text) table for finer granularity, which allows retrieving specific paragraphs rather than the entire chapter.

      Structural changes for cross-referencing:

      • Add a References table: References(from_book_ISBN, from_chapter_num, to_book_ISBN, to_chapter_num) to model cross-references between chapters. Both foreign key pairs reference Chapters.
      • This enables queries like “find all chapters that reference chapter X” or “build a citation graph”.

      Benefits of enforcing consistency rules:

      1. Referential integrity ensures that cross-references point to chapters and books that actually exist in the database. Without this, broken references accumulate.
      2. Chapter number uniqueness (enforced by the composite primary key) prevents duplicate chapter numbers for the same book, which would confuse cross-references.
      3. Positive page counts can be enforced with a CHECK(page_count > 0) constraint, preventing nonsensical data entry.
      4. Cascading deletes can be configured: deleting a book automatically removes its chapters and any cross-references involving those chapters, maintaining database consistency without manual cleanup.

      Question 2 [10 marks]

      (a) CAP theorem [3 marks]

      (i) State and explain the CAP theorem.

      Answer:

      The CAP theorem (Brewer, 2000) states that a distributed data system can provide at most two of the following three guarantees simultaneously:

      • Consistency (C): All nodes see the same data at the same time. After a write completes, all subsequent reads (from any node) return the written value. Equivalent to linearisability.
      • Availability (A): Every request received by a non-failing node must result in a response (without error). The system remains operational and responsive.
      • Partition tolerance (P): The system continues to operate despite arbitrary message loss or failure of part of the system (a network partition).

      It is impossible to achieve all three because: if a network partition occurs, a write to one side of the partition cannot be propagated to the other side. The system must either (a) refuse the write or read on one side (sacrificing Availability), or (b) allow reads to return stale data on the non-updated side (sacrificing Consistency). There is no way to maintain both consistency and availability across a partition.

      Many systems label themselves CP (consistent and partition-tolerant, e.g., HBase, BigTable) or AP (available and partition-tolerant, e.g., Dynamo, Cassandra). The “CA” choice is essentially a non-partitioned system and cannot exist in a distributed network.

      (b) ‘Relation’ in discrete maths vs databases [3 marks]

      Compare the meaning of ‘relation’ and ‘one-to-one’ in discrete mathematics and in databases.

      Answer:

      Relation in discrete maths: A relation RR on set SS is a subset of the Cartesian product S×SS \times S. A relation can have any subset of pairs. A relation is reflexive if (x,x)R(x, x) \in R for every xSx \in S.

      Example: “is the same age as” on a set of people. Every person is the same age as themselves, so this relation is reflexive. “Is the parent of” is not reflexive because no one is their own parent.

      Relation in databases: A relation is a subset of the Cartesian product of domains: a table with columns and rows. In the relational model, the term “relation” corresponds to the mathematical concept --- each row is an n-tuple, and the relation is the set of all such tuples.

      A database may also have self-referencing (recursive) relationships: a foreign key from a table to itself. Example: Employee(emp_id, name, manager_id) where manager_id is a foreign key referencing emp_id. This models a “reports to” relationship within the same entity type. Unlike the mathematical reflexive relation, not all employees have this self-relationship: the CEO has NULL for manager_id, and only some employees are managers of others.

      One-to-one in discrete maths: A bijection (one-to-one correspondence) between two sets --- every element of the first set maps to exactly one element of the second, and vice versa.

      One-to-one in databases: A one-to-one relationship between two entity types, e.g., Employee has one Desk, and each Desk is assigned to one Employee. This is implemented with a foreign key in either table plus a UNIQUE constraint on the foreign key column. It is used when two entities have a tight coupling but are logically distinct (different lifetimes, different attributes). In a 1:1 relationship, not every employee must have a desk (the FK can be NULL), and not every entity participates --- unlike a bijection.

      (c) Normalisation and redundancy trade-off [2 marks]

      Given relations R1(A, B, C, D), R2(A, B, C), R3(A, B, D, E, F), R4(A, D), where F is predictable from E. Criticise the design. If updates are much rarer than reads, might you keep the redundancy?

      Answer:

      Criticism: Since F is predictable from E, the pair (E, F) contains a functional dependency EFE \rightarrow F. In R3, E and F are both non-key attributes, and E determines F. This is a transitive dependency (or partial dependency if E is part of the key), violating 3NF (or BCNF, depending on the keys). The redundancy means that whenever E is stored with a given value, F must be stored with the same value, creating the risk of update anomalies: changing F for one row without updating all rows with the same E leads to inconsistency.

      Normalised design: Split R3 into:

      • R3a(A, B, D, E) --- the original relation without F.
      • R3b(E, F) --- a lookup table for the E → F mapping, with E as primary key.

      Now F appears exactly once per value of E. An update to the mapping only touches one row.

      Keeping the redundancy: If updates are much rarer than reads, there is an argument for denormalisation. Storing F alongside E in R3 avoids a join with R3b on every read. The trade-off is:

      • For reads: faster (no join needed; single table scan or index lookup).
      • For writes: need to update F in multiple rows in R3 (or accept eventual consistency if updates are batched). The application must guarantee that all rows with the same E get the same F.

      This is the classic OLAP vs OLTP trade-off: OLTP workloads (many small writes) favour normalisation; OLAP workloads (large read-only queries) can tolerate controlled redundancy for query performance. But the designer must understand and document the redundancy so future maintainers know to update F consistently.

      (d) Modelling IsA hierarchies [2 marks]

      Describe three approaches to modelling a two-level IsA hierarchy in a relational database. Which scales best for multi-level hierarchies?

      Answer:

      Consider a hierarchy: Person (supertype) with subtypes Student and Staff, and Staff further subtyped into Academic and Administrator.

      Approach 1: One table per type in the hierarchy (ER style).

      • Person(person_id, name, dob) --- common attributes.
      • Student(person_id, matric_year, college) --- student-specific attributes, FK to Person.
      • Staff(person_id, office, salary) --- staff-specific, FK to Person.
      • Academic(person_id, department, research_area) --- FK to Staff.
      • Administrator(person_id, department, role) --- FK to Staff.

      Each entity has exactly the attributes relevant to it. Queries about a specific subtype require joining to the supertype table. Adding a new level to the hierarchy means adding one new table for that level.

      Approach 2: Single table with type discriminator and nullable columns.

      • Person(person_id, name, dob, type, matric_year, college, office, salary, department, research_area, role)
      • type column holds values like ‘Student’, ‘Academic’, ‘Administrator’.
      • Columns irrelevant to a given type are set to NULL.

      Queries are simple (no joins), but the table has many nullable columns, and constraints (“a Student must have a matric year”) are difficult to enforce declaratively.

      Approach 3: One table per concrete subtype only (no supertype table).

      • Student(student_id, name, dob, matric_year, college) --- duplicates Person attributes.
      • Academic(staff_id, name, dob, office, salary, department, research_area) --- duplicates Person + Staff attributes.
      • Administrator(staff_id, name, dob, office, salary, department, role) --- duplicates Person + Staff attributes.

      Each table is self-contained, but common attributes are duplicated across multiple tables, and querying “all people” requires a UNION.

      Scalability for multi-level hierarchies: Approach 1 scales best. Each level in the hierarchy adds exactly one table, and attributes are never duplicated. Adding a new subtype requires only a new table with a foreign key to its parent. Approach 2 becomes increasingly unwieldy as the number of nullable columns grows with each level, and the type discriminator must encode the full hierarchy path. Approach 3 explodes combinatorially: attributes from every ancestor level are duplicated in every leaf table, and the number of leaf tables grows with the hierarchy depth.

    • Tripos Worked Solutions: 2025 Paper 3 Questions 1 and 2

      Question: Databases (Dr Timothy Griffin) --- 2 questions, 10 marks each

      Question 1 [10 marks]

      (a) SQL for relational algebra expression [2 marks]

      The expression X(YZ)X \cap (Y \cup Z) is proposed to be translated to SQL as:

      SELECT X.A
      FROM X
      JOIN Y
      JOIN Z
      WHERE X.A = Y.A OR X.A = Z.A;

      Criticise this translation. Provide a correct SQL formulation.

      Answer:

      Criticism of the proposed translation:

      The proposed SQL has multiple flaws:

      1. Missing join conditions. The JOIN clauses lack ON conditions, producing Cartesian products among X, Y, and Z. For tables with X|X|, Y|Y|, Z|Z| rows, the intermediate result has X×Y×Z|X| \times |Y| \times |Z| rows before the WHERE filter is applied. This is both semantically wrong and catastrophically inefficient.

      2. OR condition does not compute intersection of union. The WHERE X.A = Y.A OR X.A = Z.A selects rows where X.A matches something in Y or in Z. This computes X(YZ)X \cap (Y \cup Z) incorrectly for two reasons: (a) a row in X could match Y but not Z and still be included (which is correct for X(YZ)X \cap (Y \cup Z) --- but the cross join amplifies counts), and (b) if a row in X matches both Y and Z, it will appear twice in the result (once from the Y match, once from the Z match), which violates set semantics.

      3. No DISTINCT. Even with correct joins, the result would contain duplicates if a value of X.A appears in both Y and Z, or appears multiple times in Y or Z.

      Correct SQL formulations:

      Approach 1: Using INTERSECT and UNION.

      SELECT X.A FROM X
      JOIN Y ON X.A = Y.A
      INTERSECT
      SELECT X.A FROM X
      JOIN Z ON X.A = Z.A;

      This directly implements X(YZ)X \cap (Y \cup Z): first select X.A values that appear in Y, then intersect with X.A values that appear in Z. But wait --- does this equal X(YZ)X \cap (Y \cup Z)? Let us verify: X(YZ)=(XY)(XZ)X \cap (Y \cup Z) = (X \cap Y) \cup (X \cap Z). The INTERSECT of “(X join Y) intersect (X join Z)” gives (XY)(XZ)=XYZ(X \cap Y) \cap (X \cap Z) = X \cap Y \cap Z, which is wrong --- it is missing elements that are only in one of Y or Z.

      So the correct approach is:

      (SELECT DISTINCT X.A FROM X JOIN Y ON X.A = Y.A)
      UNION
      (SELECT DISTINCT X.A FROM X JOIN Z ON X.A = Z.A);

      This correctly computes (XY)(XZ)=X(YZ)(X \cap Y) \cup (X \cap Z) = X \cap (Y \cup Z). The DISTINCT in each branch is optional if UNION (rather than UNION ALL) is used, since UNION eliminates duplicates by default. However, including DISTINCT in the sub-selects can reduce the size of intermediate results.

      Approach 2: Using subqueries with IN.

      SELECT DISTINCT X.A
      FROM X
      WHERE X.A IN (SELECT Y.A FROM Y)
         OR X.A IN (SELECT Z.A FROM Z);

      This is closer to the intent of the original but correctly avoids the Cartesian product. Note the use of OR, which matches the \cup in the expression.

      Approach 3: Using EXISTS (most explicit).

      SELECT DISTINCT X.A
      FROM X
      WHERE EXISTS (SELECT 1 FROM Y WHERE Y.A = X.A)
         OR EXISTS (SELECT 1 FROM Z WHERE Z.A = X.A);

      (b) Representing lists and sets [3 marks]

      (i) Describe two differences between sets and lists.

      Answer:

      1. Order: Lists are ordered collections; the position of each element matters. Sets are unordered; an element either belongs to a set or does not, with no notion of position.
      2. Duplicates: Lists may contain duplicate elements (a value can appear at multiple positions). Sets contain each value at most once; adding a value already in the set has no effect.

      (ii) Design relational schemas for representing a list and a set.

      Answer:

      List schema:

      Lists(list_name, position, value)
            _________  ________

      Primary key: (list_name, position). Each list is identified by list_name; within each list, position (a natural number, typically 1-based) gives the index of the element. The value at that position is stored in value. The primary key ensures at most one value per position per list. To preserve list semantics, positions should be consecutive (no gaps) and start from 1, but this constraint is procedural rather than declarative.

      Set schema:

      Sets(set_name, value)
           ________  _____

      Primary key: (set_name, value). Each set is identified by set_name; each value appears at most once in each set. The primary key directly enforces uniqueness. No position column is needed.

      (iii) Interpretation of two potential schemas.

      Schema Foo(N, T, A): Primary key (N, T).

      If N is a position (natural number) and T is a list identifier, then Foo represents a list (ordered by N). Alternatively, if T names a set and A is an item, then N being part of the key would make each (T, N) unique, meaning two items cannot share the same position within a set --- but since sets have no positions, using N (a position) in the key is semantically odd. Foo could also represent a set of lists: T names a list, N gives the position within that list, and A is the value. The key (N, T) ensures one value per position per list. Foo can also represent a list of sets if we interpret T as a set name and N as the list position --- but then (T, A) being unique (via the key) is not guaranteed, so Foo works better for lists.

      Schema Bar(N, T, A): Primary key (T, A).

      This schema cannot precisely represent a list of lists. With key (T, A), each (set, value) pair is unique, which is the definition of a set. If T is a set of sets identifier, Bar represents a set of sets. To represent a list of lists, we would need both the position and the list name in the key, as in Foo.

      To represent a list of lists, we need two levels of ordering:

      ListOfLists(outer_position, inner_list_name, inner_position, value)

      Or equivalently, two tables: one for the outer list ordering and one for the inner lists.

      (c) Outer join and three-valued logic [5 marks]

      (i) Give an example demonstrating that outer join is not commutative (LEFT vs RIGHT).

      Answer:

      Consider:

      Students(crsid, name, supervisor_id)
      Supervisors(supervisor_id, name)
      
      Students LEFT JOIN Supervisors ON Students.supervisor_id = Supervisors.supervisor_id

      This returns all students, with supervisor details where available (NULL for students without a supervisor). Students without a supervisor are preserved.

      Students RIGHT JOIN Supervisors ON Students.supervisor_id = Supervisors.supervisor_id

      This returns all supervisors, with student details where available (NULL for supervisors without students). Supervisors without students are preserved.

      The two results are different sets of tuples (one preserves unmatched students, the other preserves unmatched supervisors). Therefore outer join is not commutative.

      (ii) Define equijoin using two-valued logic.

      Given R(A,B)R(A, B) and S(B,C)S(B, C), the equijoin (natural join restricted to equality on a specific attribute, here B) is:

      RS={t=(a,b,c)u=(a,b)R,  v=(b,c)S,  u.b=v.b}R \bowtie S = \{ t = (a, b, c) \mid \exists u = (a, b) \in R,\; \exists v = (b', c) \in S,\; u.b = v.b' \}

      where the comparison u.b=v.bu.b = v.b' is evaluated in two-valued logic (TRUE or FALSE). Tuples where the comparison evaluates to TRUE are included; tuples where it evaluates to FALSE are excluded.

      In tuple-relational calculus notation:

      {(u.A,u.B,v.C)uRvSu.B=v.B}\{ (u.A, u.B, v.C) \mid u \in R \land v \in S \land u.B = v.B \}

      (iii) Define left outer join using three-valued logic.

      Given R(A,B)R(A, B) and S(B,C)S(B, C), the left outer join R ⟕ SR \ ⟕ \ S is:

      R ⟕ S=(RS)    {(u.A,u.B,ω,ω,)uR¬vS,  u.B=v.B}R \ ⟕ \ S = (R \bowtie S) \;\cup\; \{(u.A, u.B, \omega, \omega, \ldots) \mid u \in R \land \neg\exists v \in S,\; u.B = v.B \}

      where ω\omega (or NULL) is used for padding unmatched tuples.

      In the context of three-valued logic, the equality u.B=v.Bu.B = v.B can evaluate to:

      • TRUE if both operands are non-NULL and equal.
      • FALSE if both operands are non-NULL and not equal.
      • UNKNOWN (or NULL, denoted \bot) if either operand is NULL.

      The natural join component only includes tuples where the equality evaluates to TRUE. Tuples where the equality evaluates to FALSE or UNKNOWN are excluded from the inner join. The outer join then adds back tuples from R that had no match (where for every vSv \in S, the equality was either FALSE or UNKNOWN), padded with NULL values for all attributes of S.

      Formally, using three-valued logic:

      R ⟕ S={tuR:  ((vS:  [u.B=v.B]=t=(u.A,u.B,v.C))    ((vS:  [u.B=v.B]=[u.B=v.B]=)t=(u.A,u.B,,)))}R \ ⟕ \ S = \{ t \mid \exists u \in R:\; ((\exists v \in S:\; [u.B = v.B] = \top \land t = (u.A, u.B, v.C)) \;\lor\; ((\forall v \in S:\; [u.B = v.B] = \bot \lor [u.B = v.B] = \bot) \land t = (u.A, u.B, \bot, \bot))) \}

      In simpler terms: for each tuple uu in R, check if there exists any vv in S where the equality is TRUE. If yes, output the joined tuple. If no, output uu padded with NULLs for the S attributes. The three-valued logic aspect is that NULLs in either B column cause comparisons to evaluate to UNKNOWN, which is not TRUE, so NULL-containing rows do not join with each other (even two NULLs do not match, since NULL = NULL evaluates to UNKNOWN, not TRUE).


      Question 2 [10 marks]

      (a) Vineyard database across multiple models [5 marks]

      A vineyard produces wines from grapes. Each wine has a name, vintage, and alcohol percentage. Each grape variety has a name and colour. Each wine can be made from multiple grape varieties, each in a specific ratio (percentage). Wines are bottled; each bottle type has a shape and volume. Each wine can be bottled in multiple bottle types.

      (i) Draw the ER diagram.

      Answer:

      Wine-Grape-Bottle ER Diagram: Wine is made from multiple Grapes (N:M with ratio attribute) and bottled in a Bottle type (N:1)

      • Wine and Grape have a many-to-many relationship Contains with attribute ratio (the percentage of that grape in the wine).
      • Wine and Bottle have a many-to-one relationship Bottled_In (each wine is bottled in one type of bottle; each bottle type can hold many wines). If a wine can be bottled in multiple bottle types, the cardinality would be many-to-many with a junction entity, but the question says “each wine can be bottled in multiple bottle types”, so the ER should actually show N:M. Let us revise:

      Revised Wine-Bottle ER Diagram: many-to-many relationship (BottledIn) between Wine and Bottle types

      (ii) RDBMS implementation with a limit of three grape types per wine.

      Answer:

      With a hard limit of three grape varieties per wine, we can embed the grape references directly in the Wines table:

      CREATE TABLE Wines (
          wine_id       INTEGER PRIMARY KEY,
          name          VARCHAR(100) NOT NULL,
          vintage       INTEGER NOT NULL,
          alcohol_pct   DECIMAL(4,1),
          grape1_id     INTEGER,
          grape2_id     INTEGER,
          grape3_id     INTEGER,
          ratio1        INTEGER CHECK (ratio1 >= 0 AND ratio1 <= 100),
          ratio2        INTEGER CHECK (ratio2 >= 0 AND ratio2 <= 100),
          ratio3        INTEGER CHECK (ratio3 >= 0 AND ratio3 <= 100),
          FOREIGN KEY (grape1_id) REFERENCES Grapes(grape_id),
          FOREIGN KEY (grape2_id) REFERENCES Grapes(grape_id),
          FOREIGN KEY (grape3_id) REFERENCES Grapes(grape_id)
      );

      Consistency rules:

      • ratio1 + ratio2 + ratio3 = 100 for each wine (assuming all three columns are used; if fewer than three grapes, unused ratios should be 0, and the total of the non-zero ratios must still be 100).
      • grape1_id, grape2_id, grape3_id must all be distinct (no duplicate grapes within the same wine). This is hard to enforce declaratively but can be checked with a trigger or application logic.
      • Unused grape columns should be NULL, and the corresponding ratio should be 0 or NULL.

      (iii) Unrestricted RDBMS implementation (any number of grapes per wine).

      Answer:

      CREATE TABLE Wines (
          wine_id       INTEGER PRIMARY KEY,
          name          VARCHAR(100) NOT NULL,
          vintage       INTEGER NOT NULL,
          alcohol_pct   DECIMAL(4,1)
      );
      
      CREATE TABLE Grapes (
          grape_id    INTEGER PRIMARY KEY,
          name        VARCHAR(50) NOT NULL,
          colour      VARCHAR(20)
      );
      
      CREATE TABLE WineGrapes (
          wine_id   INTEGER NOT NULL,
          grape_id  INTEGER NOT NULL,
          ratio     INTEGER NOT NULL CHECK (ratio > 0 AND ratio <= 100),
          PRIMARY KEY (wine_id, grape_id),
          FOREIGN KEY (wine_id) REFERENCES Wines(wine_id),
          FOREIGN KEY (grape_id) REFERENCES Grapes(grape_id)
      );
      
      CREATE TABLE Bottles (
          bottle_id   INTEGER PRIMARY KEY,
          shape       VARCHAR(30),
          volume      INTEGER  -- in ml
      );
      
      CREATE TABLE Bottling (
          wine_id    INTEGER NOT NULL,
          bottle_id  INTEGER NOT NULL,
          PRIMARY KEY (wine_id, bottle_id),
          FOREIGN KEY (wine_id) REFERENCES Wines(wine_id),
          FOREIGN KEY (bottle_id) REFERENCES Bottles(bottle_id)
      );

      The junction table WineGrapes handles any number of grape varieties per wine. The composite primary key (wine_id, grape_id) ensures each grape appears at most once per wine. The Bottling junction table handles the many-to-many relationship between wines and bottle types.

      A constraint to ensure total ratios sum to 100 requires either a trigger or application-level validation, since SQL CHECK constraints cannot reference other rows.

      (iv) Graph database representation using Cypher.

      Answer:

      CREATE
        (w:Wine {name: 'Château Margaux', vintage: 2015, alcohol_pct: 13.5}),
        (g1:Grape {name: 'Cabernet Sauvignon', colour: 'Red'}),
        (g2:Grape {name: 'Merlot', colour: 'Red'}),
        (b:Bottle {shape: 'Bordeaux', volume: 750}),
        (w)-[:CONTAINS {ratio: 80}]->(g1),
        (w)-[:CONTAINS {ratio: 15}]->(g2),
        (w)-[:BOTTLED_IN]->(b);

      Each entity becomes a node with a label (:Wine, :Grape, :Bottle). Attributes become properties on the nodes. Relationships become typed edges with optional properties (ratio on CONTAINS). The many-to-many relationships are naturally represented as edges.

      Cypher queries:

      -- Find all wines containing Cabernet Sauvignon
      MATCH (w:Wine)-[:CONTAINS]->(:Grape {name: 'Cabernet Sauvignon'})
      RETURN w.name, w.vintage;
      
      -- Find the composition of a specific wine
      MATCH (w:Wine {name: 'Château Margaux'})-[c:CONTAINS]->(g:Grape)
      RETURN g.name, c.ratio
      ORDER BY c.ratio DESC;

      (v) Relative merits of the approaches.

      Hardcoded three-grape approach:

      • Advantages: Simple schema, no joins needed to get grape composition, fast reads for queries that always need all grape data.
      • Disadvantages: Inflexible --- adding a fourth grape requires a schema change (ALTER TABLE). Wastes storage (NULL columns for wines with fewer than three grapes). Complex query logic needed to handle variable numbers of grapes (“which wines use Merlot?” requires WHERE grape1_id = ? OR grape2_id = ? OR grape3_id = ?).

      Junction table approach:

      • Advantages: Fully flexible (any number of grapes per wine). Normalised --- each grape reference stored once. Simple queries using joins. Adding a grape variety is just a data change, not a schema change.
      • Disadvantages: Requires joins for queries that need grape data alongside wine data. Aggregate queries (“total production using Merlot”) require GROUP BY on the junction table, which is more expensive than a column scan.

      Graph database approach:

      • Advantages: Intuitive model for many-to-many relationships --- grape composition is directly visible as edges. Path queries are natural (“find wines that share at least two grape varieties”). No join tables needed.
      • Disadvantages: Less suited for aggregate queries (“total production volume by grape type across all wines”). Property graph databases historically had weaker support for aggregations and transactions compared to RDBMSs. Schema-less nature means less enforcement of data integrity.

      (b) JSON in relational databases for recipes [5 marks]

      (i) Compare storing recipes as (name, text) pairs vs adding a structured JSON ingredients field.

      Answer:

      Traditional approach: Recipes(recipe_id, name, instructions_text). The full recipe (including ingredient list and instructions) is stored as free text in instructions_text.

      • Advantages: Simple to implement and query. Full-text search can find recipes mentioning “chicken” without parsing. No additional modelling effort.
      • Disadvantages: Cannot run structured queries like “find all recipes using chicken and requiring less than 500g of flour”. Changing units (e.g., imperial to metric) requires parsing free text. Ingredient substitutions (e.g., “replace butter with margarine”) cannot be done programmatically without brittle text parsing.

      With JSON ingredients field: Recipes(recipe_id, name, instructions_text, ingredients JSON). The ingredients column stores a structured representation, e.g.:

      [
        {"name": "chicken breast", "quantity": 500, "unit": "g"},
        {"name": "olive oil", "quantity": 2, "unit": "tbsp"},
        {"name": "garlic", "quantity": 3, "unit": "clove"}
      ]
      • Advantages: Enables structured queries on ingredients (by name, quantity, unit). Supports ingredient scaling (“double the recipe” --- multiply all quantities by 2). Can join with a nutritional database to compute calories per serving. The free-text instructions field remains for the procedural steps.
      • Disadvantages: More complex to maintain. Risk of the JSON and text getting out of sync (the text mentions an ingredient not in the JSON, or vice versa). Schema validation for JSON is weaker than for relational columns. Updates to individual ingredients within the JSON array are awkward in SQL (usually requires extracting, modifying, and replacing the entire JSON value).

      (ii) Problems with JSON-in-RDBMS and two useful JSON expansion mechanisms.

      Answer:

      Problems with JSON-in-RDBMS:

      1. Opaque to the query planner. Traditional B-tree indexes cannot index values nested inside JSON columns. Without a specialised JSON index (e.g., PostgreSQL GIN index on jsonb), queries that filter on JSON fields require full table scans, reading and parsing every JSON document.
      2. Weak schema guarantees. JSON columns do not enforce that documents conform to a particular structure. The database cannot guarantee that every document has an ingredients array, or that each element has name, quantity, and unit fields. This shifts validation burden to the application.
      3. Complex updates. Updating a single field within a nested JSON structure usually requires extracting the entire document, modifying it in application code, and writing it back. This is non-atomic (unless wrapped in a transaction), slow, and risks concurrent-update conflicts.
      4. Awkward joins. Joining JSON data with relational data requires extracting JSON fields into relational form first. The syntax for this is database-specific and often cumbersome.

      Two useful JSON expansion mechanisms:

      1. json_each() / json_table() (expanding arrays to rows).

      These functions expand a JSON array into a set of rows, turning nested structure into flat relational form. Each element of the array becomes one row, with columns for the element value and, optionally, its index.

      Example (PostgreSQL syntax with jsonb_array_elements):

      SELECT recipe_id,
             ingredient->>'name' AS name,
             (ingredient->>'quantity')::INT AS quantity,
             ingredient->>'unit' AS unit
      FROM recipes,
           jsonb_array_elements(ingredients) AS ingredient;

      This produces one row per ingredient per recipe, which can then be filtered, grouped, and joined like any other relational data. For the recipe database, this would let us create a view that flattens the JSON ingredients into relational form, enabling queries like:

      SELECT r.name
      FROM recipes r,
           jsonb_array_elements(r.ingredients) AS ing
      WHERE ing->>'name' = 'chicken breast'
        AND (ing->>'quantity')::INT <= 300;

      2. json_extract() / -> operator (extracting individual fields).

      These extract a specific field from a JSON value, returning it as a relational scalar. This is useful for pulling out top-level or nested fields into query results.

      Example (PostgreSQL):

      SELECT recipe_id,
             ingredients->0->>'name' AS first_ingredient,
             ingredients->0->>'quantity' AS first_quantity
      FROM recipes;

      This extracts the name and quantity of the first ingredient from each recipe’s JSON array. The -> operator navigates the JSON structure; ->> returns the result as text.

      For recipes, json_extract could pull out metadata like total cooking time from a nested JSON field, or extract a specific ingredient’s quantity for scaling calculations:

      SELECT name,
             (SELECT SUM((ing->>'quantity')::INT)
              FROM jsonb_array_elements(ingredients) AS ing) AS total_quantity
      FROM recipes;

      This computes the total quantity of all ingredients (summing across all units naively --- unit conversion would require additional logic).