06-763 / L3

Lecture 3: Relational data & SQL

Week 2, Data Systems

Systems and Toolchains for AI Engineers

Systems and Toolchains for AI Engineers
06-763 / L3

Roadmap

  1. Why a database, not a spreadsheet
  2. The relational model: long beats wide
  3. Types that carry physical meaning
  4. SQL that answers engineering questions
  5. Loading, indexes, and EXPLAIN ANALYZE
  6. Live demo: a month of sensor data
Systems and Toolchains for AI Engineers
06-763 / L3

Why a database

Systems and Toolchains for AI Engineers
06-763 / L3

Why a database

  • Which sensor ran hottest last Tuesday?
  • How many readings did node 12 drop overnight?
  • What was the hourly average across the floor?

The arithmetic is trivial.
The shape of the data is the hard part.

Systems and Toolchains for AI Engineers
06-763 / L3

Why a database, the obvious shape: one column per sensor

ts m1 m2 m3 … m54
08:00 19.9 20.1 NULL … 18.7

Looks tidy with four sensors.
Falls apart on a real deployment.

Systems and Toolchains for AI Engineers
06-763 / L3

Why a database, why wide falls apart

  • New node → add a column → every query breaks
  • Node offline → column of blanks (offline? or zero?)
  • "Which sensors exceeded 30°C?" → no answer

You cannot put a WHERE clause on a column name.

Systems and Toolchains for AI Engineers
06-763 / L3

Why a database, the quieter failure

A CSV has no opinion about what belongs in a cell.

This week's data: 2.3M readings, 54 motes,
Intel Berkeley Lab, February to April 2004.

Hiding in it: temperatures of 122°C and 386°C,
in the same column as the honest 19°C ones.

Nothing marks them impossible.

Intel Lab Data

Systems and Toolchains for AI Engineers
06-763 / L3

Why a database, a schema is a contract

A database lets you state, once and enforceably:

  • this column is a timestamp with a time zone
  • this one is a number
  • this sensor_id must refer to a sensor that exists

A contract you can query turns a pile of numbers
into something you can answer questions from and defend.

Systems and Toolchains for AI Engineers
06-763 / L3

Why a database, a question

You kept the wide table, one column per mote, m1 through m54. Which question can you not answer with a WHERE clause?

  1. What was mote 12's average temperature on 3 March?
  2. Which motes ever exceeded 30°C?
  3. How many rows have no value for mote 12?
  4. What was the highest reading mote 7 ever produced?
Systems and Toolchains for AI Engineers
06-763 / L3

The relational model

Systems and Toolchains for AI Engineers
06-763 / L3

The relational model

Long format: one row per (entity, time, quantity) measurement, rather than one column per sensor.

Primary key: a column, or a small set, whose value uniquely identifies a row, so there is exactly one row per key.

Keys

  • Primary key: uniquely identifies a row (sensor_id, ts, …)
  • Foreign key: must match a key elsewhere

A reading for sensor 5 cannot exist unless sensor 5 does.
Enforced on every insert.

Systems and Toolchains for AI Engineers
06-763 / L3

The relational model, integrity, enforced for you

INSERT INTO readings VALUES (999, now(), 'temperature', 21.0);
-- ERROR: violates foreign key constraint "readings_sensor_id_fkey"

Foreign key: a column whose value must already exist in another table, so a reading cannot name a mote that does not exist.

A mote that isn't in sensors is rejected, not silently stored.
And (sensor_id, ts, variable) as the primary key means
the same reading twice is a duplicate the database refuses.

Systems and Toolchains for AI Engineers
06-763 / L3

The relational model, normalization

Normalization: storing each fact once, in the place it belongs, and referring to it by key everywhere else.

  • Location & unit don't change per reading → sensors table
  • Readings carry only a sensor_id pointer

Fix a calibration in one row, not two million.

Systems and Toolchains for AI Engineers
06-763 / L3

Systems and Toolchains for AI Engineers
06-763 / L3

The relational model, the long form, in SQL

CREATE TABLE sensors (             -- static per-mote metadata
    sensor_id int PRIMARY KEY,
    x_m double precision, y_m double precision
);
CREATE TABLE readings (            -- one row per (mote, time, quantity)
    sensor_id int NOT NULL REFERENCES sensors (sensor_id),
    ts        timestamptz NOT NULL,
    variable  text NOT NULL,       -- 'temperature', 'voltage', …
    value     double precision,
    PRIMARY KEY (sensor_id, ts, variable)
);

Units and plausible ranges live in a small variables table.

Systems and Toolchains for AI Engineers
06-763 / L3

The relational model, tidy vs typed

Fully tidy Typed columns
variable, value temperature, humidity, …
new channel = new rows new channel = ALTER TABLE
one type for all a type per channel

Both beat wide. Pick deliberately; justify it in Assignment 2.

Systems and Toolchains for AI Engineers
06-763 / L3

The relational model, the pitfall to resist

Column names should never be entities
(sensor ids, machine ids, run numbers).

A column is a kind of thing you measure,
never a particular thing you measured it from.

Entities go in rows.

Systems and Toolchains for AI Engineers
06-763 / L3

The relational model, a question

A classmate declares PRIMARY KEY (sensor_id, ts) on readings. Every mote reports temperature and humidity on the same 31-second tick. What happens?

  1. Nothing, the key is fine
  2. Humidity is rejected, because temperature already claimed that (mote, instant)
  3. Two motes can no longer report at the same instant
  4. Inserts slow down as the table grows
Systems and Toolchains for AI Engineers
06-763 / L3

Types that carry meaning

Systems and Toolchains for AI Engineers
06-763 / L3

Types that carry meaning

timestamptz: an instant in time stored in UTC, so it means the same thing wherever it is read.

timestamp = wall clock, no zone → ambiguous
timestamptz = an instant, stored UTC → well defined

2004-03-14 02:30:00 on a clock-change night
happened twice, or never.

Store instants in UTC. Always.

PostgreSQL: date/time types

Systems and Toolchains for AI Engineers
06-763 / L3

Types that carry meaning, the leap second of 2012

Case: the leap second of 30 June 2012

  • Last minute of the day ran to 23:59:60
  • A Linux kernel bug → servers spun at 100% CPU
Systems and Toolchains for AI Engineers
06-763 / L3

Types that carry meaning, who fell over and who didn't

Reddit, LinkedIn, Mozilla, Yelp, Foursquare, StumbleUpon

Amadeus Altea reservation system offline ~1 hour
→ Qantas & Virgin Australia checked in passengers by hand

The Register, 2 Jul 2012

Google saw it coming: spread the extra second across
many tiny clock adjustments. A "leap smear."

Lesson at every scale:

Calendar arithmetic is full of edge cases.
Don't be the one reimplementing it.

Store UTC, use timestamptz, let tested code do the math.

Systems and Toolchains for AI Engineers
06-763 / L3

Types that carry meaning, numeric vs double precision

double precision numeric
64-bit float, inexact exact decimal
fast, compact slow, large
sensor values money, exact calibration

Measurement uncertainty ≫ float error.
double precision is the right default for physical data.

PostgreSQL: numeric types

Systems and Toolchains for AI Engineers
06-763 / L3

Types that carry meaning, interval: a real duration

ts - lag(ts) yields an interval, not a bare number.

Compare it straight to '1 hour' or '31 seconds'.
No juggling epoch seconds by hand.

Systems and Toolchains for AI Engineers
06-763 / L3

Types that carry meaning, a typed column is not a validated one

Type versus constraint: a type says what shape a value has; a CHECK constraint says which values are allowed. Only the second one knows the instrument.

double precision accepts 386°C as happily as 19.

386 is a perfectly good float.

You need a CHECK constraint or a range filter,
and a threshold that knows the instrument.

Systems and Toolchains for AI Engineers
06-763 / L3

Types that carry meaning, push the check into the schema

ALTER TABLE readings ADD CONSTRAINT plausible_value
  CHECK (value BETWEEN -50 AND 500);

A CHECK makes the database refuse the bad row on write.
Set the bound from the instrument, not from hope.

Systems and Toolchains for AI Engineers
06-763 / L3

Systems and Toolchains for AI Engineers
06-763 / L3

Types that carry meaning, voltage is a data-quality signal

  • Batteries drain past ~2.4 V over the month
  • Below that, the temperature channel lies
  • ~18% of temps are impossible (outside 0 to 50°C)
  • Essentially all of them: motes already below 2.4 V

The cleaning rule isn't a guess. It's in the data.

Systems and Toolchains for AI Engineers
06-763 / L3

Types that carry meaning, so store the context

Units and calibration live in the sensors table,
beside the values they explain.

A number without its provenance
cannot be validated, only believed.

Systems and Toolchains for AI Engineers
06-763 / L3

Types that carry meaning, a question

Every column is typed properly: ts timestamptz, value double precision, sensor_id int REFERENCES sensors. Which bad row does the database still accept?

  1. A reading from mote 999
  2. A temperature of 386°C
  3. A ts of 2004-02-30
  4. A value of 'n/a'
Systems and Toolchains for AI Engineers
06-763 / L3

SQL for engineering questions

Systems and Toolchains for AI Engineers
06-763 / L3

SQL for engineering questions

You describe the result you want.
The database decides how to compute it.

Same query, different plan as the data grows,
with no rewrite from you. (That's what an index changes.)

Systems and Toolchains for AI Engineers
06-763 / L3

SQL for engineering questions, the vocabulary

  • SELECT the columns
  • FROM the table, JOIN another by key
  • WHERE keeps rows
  • GROUP BY combines rows, HAVING keeps groups

Almost every sensor question is a short combination of these.

Systems and Toolchains for AI Engineers
06-763 / L3

SQL for engineering questions, time bucketing with date_trunc

Hourly average temperature per sensor:

SELECT sensor_id,
       date_trunc('hour', ts) AS hour,
       avg(value)             AS avg_temp
FROM   readings
WHERE  variable = 'temperature'
GROUP  BY sensor_id, hour;

date_trunc collapses irregular readings into regular buckets.

PostgreSQL: date/time functions

Systems and Toolchains for AI Engineers
06-763 / L3

SQL for engineering questions, HAVING: filter the groups

Dropped motes = motes with too few readings.

SELECT sensor_id, count(*) AS n
FROM   readings
WHERE  variable = 'temperature'
GROUP  BY sensor_id
HAVING count(*) < 30000
ORDER  BY n;

WHERE filters rows; HAVING filters aggregates.

Systems and Toolchains for AI Engineers
06-763 / L3

SQL for engineering questions, window functions

Window function: a computation across neighboring rows that does not collapse them, so each row keeps its identity and sees its neighbors.

Each reading keeps its identity and sees its neighbors.

lag, avg(...) OVER (...), rank, …

The part of SQL that makes time-series tractable.

PostgreSQL: window functions

Systems and Toolchains for AI Engineers
06-763 / L3

SQL for engineering questions, lag: turn gaps into a column

SELECT sensor_id, ts,
       ts - lag(ts) OVER (
         PARTITION BY sensor_id ORDER BY ts
       ) AS gap
FROM   readings
WHERE  variable = 'temperature';

Motes report ~every 31 s.
Any gap ≫ 31 s is a dropout, located in time.

Systems and Toolchains for AI Engineers
06-763 / L3

SQL for engineering questions, rolling average, by time not rows

avg(value) OVER (
    PARTITION BY sensor_id ORDER BY ts
    RANGE BETWEEN INTERVAL '1 hour' PRECEDING
              AND CURRENT ROW
)

Sampling is irregular → a time frame is the honest one.
PostgreSQL writes it almost as you'd say it.

Systems and Toolchains for AI Engineers
06-763 / L3

SQL for engineering questions, a question

You compute ts - lag(ts) OVER (ORDER BY ts) and forget PARTITION BY sensor_id. What comes back?

  1. An error, lag requires a partition
  2. Gaps near zero, because each row's predecessor is usually a different mote
  3. The same answer, ORDER BY ts already groups each mote together
  4. NULL in every row
Systems and Toolchains for AI Engineers
06-763 / L3

SQL for engineering questions, WHERE + CASE: flag, don't just drop

WHERE r.value < 0 OR r.value > 50   -- impossible indoors
CASE WHEN value BETWEEN 0 AND 50
     THEN 'ok' ELSE 'suspect' END

Label rather than discard, when that's what you want.

Systems and Toolchains for AI Engineers
06-763 / L3

SQL for engineering questions, JOIN: attach the context

SELECT r.sensor_id, s.x_m, s.y_m, avg(r.value)
FROM   readings r
JOIN   sensors  s USING (sensor_id)
WHERE  r.variable = 'temperature'
GROUP  BY r.sensor_id, s.x_m, s.y_m;

The reading's value, placed where it was measured.

Systems and Toolchains for AI Engineers
06-763 / L3

SQL for engineering questions, aggregates summarize a sensor

SELECT sensor_id,
       count(*), min(value), max(value),
       avg(value), stddev(value)
FROM   readings
WHERE  variable = 'temperature'
GROUP  BY sensor_id;

DISTINCT when you want the set, not the count:
SELECT DISTINCT sensor_id FROM readings;

Systems and Toolchains for AI Engineers
06-763 / L3

Loading, indexes, query cost

Systems and Toolchains for AI Engineers
06-763 / L3

Loading, indexes, query cost

COPY: PostgreSQL's bulk loader, one file into a table in a single operation, orders of magnitude faster than a loop of INSERTs.

2M single-row INSERTs = 2M round trips = an afternoon.

COPY streams a whole file in one operation:

COPY readings (sensor_id, ts, variable, value)
FROM '/data/readings.csv'
WITH (FORMAT csv, HEADER true);

Orders of magnitude faster.

PostgreSQL: COPY

Systems and Toolchains for AI Engineers
06-763 / L3

Loading, indexes, query cost, the paths from Python

  • psql: interactive client, \copy from the client side
  • psycopg: direct driver, cursor.copy()
  • SQLAlchemy: portable layer, pairs with pandas
Systems and Toolchains for AI Engineers
06-763 / L3

Loading, indexes, query cost, real data resists the loader

This file has:

  • truncated rows (a few fields only)
  • sub-second timestamps
  • mote ids outside 1 to 54 (corrupt id field)

Staging table pattern: COPY raw → clean with SQL → insert.
Cleaning rules become queries, and the FK catches the rest.

Systems and Toolchains for AI Engineers
06-763 / L3

Loading, indexes, query cost, the dominant query: a range scan

One sensor, one window of time.

Naively: read every row, discard misses.
A sequential scan costs the whole table
even for a 100-row answer.

Systems and Toolchains for AI Engineers
06-763 / L3

Loading, indexes, query cost, the B-tree index

B-tree index: an ordered structure that keeps keys sorted, so a range lookup is a descent plus a walk and its cost tracks the result size, not the table size.

For per-sensor time ranges:

CREATE INDEX ON readings (sensor_id, ts);

Composite, in that order: cluster by sensor, sort by time.

Systems and Toolchains for AI Engineers
06-763 / L3

One sensor, one-hour window, as the table grows to 2.3M rows. ~67× at full size. Measured in SQLite (B-trees, like Postgres).

Systems and Toolchains for AI Engineers
06-763 / L3

Loading, indexes, query cost, a question

You created CREATE INDEX ON readings (sensor_id, ts). Which WHERE clause does it fail to help?

  1. sensor_id = 1 AND ts BETWEEN $1 AND $2
  2. sensor_id = 1
  3. ts BETWEEN $1 AND $2, every mote
  4. sensor_id IN (5, 12) AND ts > $1
Systems and Toolchains for AI Engineers
06-763 / L3

Loading, indexes, query cost, EXPLAIN ANALYZE: read two things

EXPLAIN ANALYZE: runs the query and prints the plan the database actually chose, annotated with real timings.

Run the query; print the real plan and timings.

  1. The scan at the base, the strategy
    (Seq Scan vs Index Only Scan)
  2. The total execution time at the bottom
EXPLAIN ANALYZE
SELECT count(*) FROM readings
WHERE sensor_id = 1
  AND ts BETWEEN '2004-03-15' AND '2004-03-16';

PostgreSQL: using EXPLAIN

Systems and Toolchains for AI Engineers
06-763 / L3

Loading, indexes, query cost, reading the plan

Before the index (9.1M rows):

Aggregate
  -> Gather  (Workers Planned: 2)
       -> Parallel Seq Scan on readings
            Filter: (sensor_id = 1 AND ts BETWEEN ...)
Execution Time: ~190 ms

After CREATE INDEX ... (sensor_id, ts):

Aggregate
  -> Index Only Scan using readings_sensor_ts
Execution Time: ~0.04 ms
Systems and Toolchains for AI Engineers
06-763 / L3

Loading, indexes, query cost, indexes are not free

  • Each one costs space
  • Each one slows every write (maintained on insert)
  • An index the planner never uses is pure overhead

Index the access patterns you have;
confirm with EXPLAIN ANALYZE.

PostgreSQL: indexes

Systems and Toolchains for AI Engineers
06-763 / L3

Loading, indexes, query cost, when time-access dominates: hypertables

TimescaleDB auto-partitions a table into
time chunks.

  • Query last week → touch only last week's chunks
  • Compress or drop old data a chunk at a time

Same SQL, same relational model, tuned for time-series.

Systems and Toolchains for AI Engineers
06-763 / L3

Loading, indexes, query cost, today's database is OLTP

Optimized for many correct writes and point/range reads:
transactions, joins, constraints, one row at a time.

Row store: PostgreSQL keeps all of a row's columns together on disk, ideal for whole rows, poor for scanning one column across the whole table.

Scanning three columns across ten years is a different job.

That's OLAP, and it wants a different store.

Systems and Toolchains for AI Engineers
06-763 / L3

Loading, indexes, query cost, a question

PostgreSQL, with the B-tree on (sensor_id, ts). Which job is it worst at?

  1. Mote 5's readings for last Tuesday
  2. Accepting 200 readings a second with the foreign key checked on each
  3. Averaging three columns over all 2.3M rows, redrawn ten times a day for a dashboard
  4. Refusing a reading from a mote that does not exist
Systems and Toolchains for AI Engineers
06-763 / L3

Demo

l03-sql-timeseries.ipynb

PostgreSQL in Docker → schema → COPY 2.3M rows
→ answer engineering questions live

Systems and Toolchains for AI Engineers
06-763 / L3

What to watch

  • date_trunc hourly averages
  • windowed lag for dropouts, rolling voltage
  • the impossible temps low voltage predicts
  • EXPLAIN ANALYZE before vs after the index

Watch the top node flip: Seq Scan → Index Only Scan.

Systems and Toolchains for AI Engineers
06-763 / L3

Recap

  • Long form: new sensor = INSERT, every question = WHERE
  • Types with meaning: timestamptz UTC, double precision
  • A type is not a validation (386°C)
  • date_trunc + window functions answer time-series
  • B-tree on (sensor_id, ts); confirm with EXPLAIN ANALYZE
Systems and Toolchains for AI Engineers
06-763 / L3

Standings

Nicknames only. Everyone who skipped one still counted in every bar you saw.

Systems and Toolchains for AI Engineers

110 min. Budget: 15 / 20 / 20 / 20 / 15 / 20 demo, ~ leave slack. Dataset is the Intel Berkeley Lab motes, ~2.3M readings. The demo is the payoff; if running long, cut the numeric-vs-double slide, not the demo. Six clicker questions, one at the end of each section. Each is a minute of voting plus whatever the discussion costs, so budget 12 to 15 minutes for the set. They come out of the section they close, not out of the demo. The two to protect are PARTITION BY and the index column order: both are mistakes students make in Assignment 2, and both fail quietly. If time is short, drop the first question (the wide table) and the last (OLTP), which are the two the room is most likely to walk in already knowing.

Closes the wide-versus-long argument by making them use it rather than nod at it. Expect B to win but not by much. The common wrong answer is C, from students who read NULL as unanswerable rather than as an ordinary IS NULL test. If the room lands in the middle band, ask a defender of C to say the query out loud. It takes about ten seconds to write, which settles it.

This is THE recurring mistake. Worth 90 seconds and a show of hands.

Tests whether a composite key is understood as a constraint on the TUPLE rather than on each column. C is the productive wrong answer, and it is worth drawing out: a student who picks it believes a key constrains its columns one at a time. Ask them which existing row collides, and the tuple falls out of the answer. Point back at the CREATE TABLE on the schema slide once the vote is in.

The single most transferable idea in this section, so give it the full window even if the room is quick. C is the interesting distractor. Students who have only ever parsed dates in Python are often unsure whether 30 February is caught, and it is: the date type rejects it outright. If they sail through, say the one line that generalizes it: a type constrains the shape of a value, a constraint constrains its meaning, and only you know the instrument.

The most expensive bug in the section, because it fails quietly: no error, no warning, and a column of small plausible intervals with every dropout erased. Expect a real split between B and C. A student who picks C is thinking of ORDER BY as a grouping, which is exactly the confusion PARTITION BY exists to resolve. This one is worth a re-vote after they argue. Then show it live in the demo: run the lag query with and without the partition and put the two gap columns side by side.

Column order in a composite index, which is the one thing about indexes that a practitioner gets wrong repeatedly. The phone-book analogy in the hint is the whole explanation, so hold it back unless they need it. Good bridge into EXPLAIN ANALYZE: the answer to 'which query does this index help' is something you confirm rather than reason about, and the demo runs exactly this comparison.

The closer, and the one that earns the OLTP slide they just saw. Everything on the list is something PostgreSQL will do. The question is which one it does badly, which is the shape of the answer they need for the rest of the course. Keep this one short, five minutes at most, because the demo is what matters next.