Break
Modern data systems
Nano quiz ↗
press b or esc to carry on
M6

The Data Systems Architect

1 figure
section1a-intro

What Module 6 solves: scale, architecture, and trust once one machine is not enough

What Module 6 solves: scale, architecture, and trust once one machine is not enough Three cyan panels naming the problems this module solves. Scale: data outgrows one machine, so you spread it across a cluster (the startup, complex-types, and big-tech-scale pages). Architecture: data structure dictates capability, choosing relational, JSONB, or vector, and one stack or many systems (the SQL-vs-NoSQL and key-value pages). Trust: data is a liability, so you keep attackers out and keep people anonymous (the security and privacy pages). Color key: cyan marks the three problems the module is organized around. What Module 6 solves Three problems that appear once one machine is no longer enough Scale Data outgrows one machine. A laptop CSV cannot hold it, so you spread it across a cluster. startup · complex types · big-tech scale Architecture Structure dictates capability. Relational, JSONB, or vector; one stack or many systems. SQL vs NoSQL · key-value stores Trust Data is a liability. Keep attackers out, and keep the people in it anonymous. data security · data privacy cyan = the three problems this module is organized around
Three constraints drive system design. Scale: a dataset exceeds one machine, so the system partitions data across a cluster and the engine must match the hardware limits at that scale. Architecture: a data model (relational, JSONB, vector) and a deployment choice (one system or several) determine which features applications can implement. Trust: a large dataset increases exposure, so the system must block attackers and preserve anonymity for people represented in the data.
Notes ↗
M6

Case Study: OpenClaw's Memory

An AI agent like OpenClaw consists of a loop and a memory.

3 figures
case-study-openclaw · An agent is a loop and a memory

An agent is a loop and a memory

An agent is a loop and a memory OpenClaw's architecture as a loop over a folder of files. On the left, the agent loop reads context, calls the model and tools, and writes to memory; each claw keeps its own folder. In the center, the memory is a folder of plain files: MEMORY.md holds curated facts loaded at session start, and dated daily-note files log what happens, appended as it goes. A background "dreaming" pass folds the good notes up into MEMORY.md, which is compaction. On the right, agent.sqlite is a derived search index (chunked text and embeddings) rebuilt from the files, so the loop finds a memory by meaning or keyword instead of re-reading everything. Color key: slate is the loop, the files, and the index; grey is the read and write paths and the notes. An agent is a loop and a memory OpenClaw's memory is a folder of plain files, with a search index beside it. The loop read context call the model, use tools write to memory until the task is done each claw keeps its own folder Memory: a folder of files MEMORY.md curated facts · loaded at session start memory/2026-07-16.md daily notes · appended as it goes canonical · plain text · the source of truth writes reads indexed agent.sqlite the search index chunks + embeddings powers memory_search derived · rebuildable dreaming = compaction slate = the loop, the files, and the index  ·  grey = the read and write paths and notes
OpenClaw stores memory in a folder on disk plus a derived SQLite index. The loop reads, acts, and writes each turn; each claw uses its own folder. The files are the canonical state: MEMORY.md holds curated facts loaded at session start, and daily notes store an append-only log. The dreaming pass merges retained notes into MEMORY.md and deletes the rest. agent.sqlite indexes chunked text and embeddings and can be rebuilt from the files.
Notes ↗
case-study-openclaw · Share the memory, and Module 4 is back

Share one memory across claws, and Module 4 is back

Share one memory across claws, and Module 4 is back Two agents writing one shared file, and the fix. On the left, claw A and claw B both write to one shared MEMORY.md. In the center, both edits hit the same file and claw B saves last, so claw A's earlier edit is struck through and labeled a lost update, the classic Module 4 anomaly. On the right, the fix is a file lock so only one writer touches the file at a time, or append-only files and conditional writes. Color key: red is the conflict, a lost update; slate is the claws and the shared file. Share one memory across claws, and Module 4 is back The default is isolation. The frontier shares memory, and the old anomalies return. claw A claw B two writes, one file shared MEMORY.md claw A saves first lost update claw B saves last, and wins so you add the Module 4 fix a file lock: one writer at a time or append-only + conditional writes red = the conflict (a lost update)  ·  slate = the claws and the shared file
Two claws write one shared MEMORY.md. Both read and both save; claw B saves last and overwrites claw A's edit. The overwrite creates a lost update. A fix applies Module 4 techniques to files: take a file lock so one writer updates at a time, or use append-only, idempotent writes so replay stays safe. Module 4, concurrency control.
Notes ↗
case-study-openclaw · Files for a few agents, a database for many

For a few agents, files. For many, a database.

For a few agents, files. For many, a database. How many agents share the memory decides the design. At the left, a few agents each keep their own folder, so files win: plain text, git-diffable, and nothing to conflict. At the right, many agents write one shared store, so every write is concurrent and a database wins: its transactions serialize the writers, as in Postgres with pgvector or Tencent's agent memory. A right-pointing arrow between them shows the field moving from few agents to many, pushed by multi-agent systems and autonomous research. Color key: slate is the two regimes; grey is the trend from few agents to many. For a few agents, files. For many, a database. How many agents share the memory decides which side wins. A few agents files win each keeps its own folder plain text, git-diffable nothing to conflict Many agents a database wins one shared store transactions serialize writes Postgres + pgvector · Tencent more agents, more sharing few many multi-agent systems and autonomous research push this way slate = the two regimes  ·  grey = the trend from few agents to many
The agent count drives the memory design. A few agents can use separate folders: plain text, git-diffable, no conflicts. Many agents require one shared store with concurrent writes. A database (Postgres with pgvector, or Tencent's agent memory) serializes writes with Module 4 transactions, and a pipeline maintains a readable copy. Multi-agent systems and autonomous research push deployments toward the many-agent end.
Notes ↗
M6

6.1 · DB Design for a Startup: Keep It Simple

Most apps stay within one machine.

1 figure
db-design-startup · The First Scaling Move: Read Replicas

Read replicas: one primary for writes, many copies for reads

Read replicas: one primary for writes, many copies for reads The application sends all writes to a single primary database and sends reads to a pool of read replicas. The primary streams its changes to the replicas asynchronously, so a replica can briefly trail the primary. Color key: blue = the primary, which takes every write; violet = the read replicas, the scaled-out copies that serve reads; dashed = asynchronous replication, the source of replica lag. Read replicas Split the read load off the write path Application clients writes Primary all writes reads Read replicas reads, ×N copies async replication · replica lag Color key  primary = all writes · replicas = scaled-out reads · dashed = async replication, the lag
Writes go to the single primary (blue). Reads spread across N replicas (violet). The primary streams its changes to the replicas asynchronously (dashed), so a replica can trail the primary by milliseconds. Add replicas to increase read capacity without changing the write path.
Notes ↗
M6

SQL's Complex Types

SQL extends beyond INT and TEXT.

1 figure
sql-beyond-tables

Same SQL shape, new data types: SELECT/FROM/WHERE stays, only the function changes

Same SQL shape, new data types: SELECT/FROM/WHERE stays, only the function changes A central cyan box holds the unchanging query shape: SELECT, FROM, WHERE, ORDER BY. Four grey domain chips hang below it, each keeping that same shape and only dropping in a domain-specific function: geographic (ST_DWITHIN), genomic (EDIT_DISTANCE), semi-structured (JSONB and arrays), and generative AI (AI.GENERATE). Color key: cyan is the query shape that never changes, grey is the domain function you swap into a clause. Same SQL shape, new data types SELECT / FROM / WHERE / ORDER BY stays; only the function in the clause changes The query shape, unchanged SELECT columns / expressions FROM tables WHERE conditions ORDER BY sort Geographic ST_DWITHIN(loc, pt, 5000) users within 5 km of a point, in WHERE Genomic EDIT_DISTANCE(a, b) < 3 DNA strings within a few mutations Semi-structured items ->> 'sku' JSONB and arrays inside one column Generative AI AI.GENERATE(prompt, col) an LLM applied to every row cyan = the query shape that never changes  ·  grey = the domain function you drop into a clause
Domain-specific types plug into the same SELECT / FROM / WHERE / ORDER BY shape through functions. A query can place a distance predicate in WHERE, extract a JSON path in SELECT, or call an LLM as a computed column.
Notes ↗
M6

Database Design at Big-Tech Scale

At big-tech scale, the data stack splits into three systems: OLTP for user-facing writes, OLAP for analytics over billions of rows, and an ETL/ELT pipeline that moves data from one to the other.

2 figures
db-design-bigtech

Three systems, one data stack: OLTP serves users, ETL/ELT moves data, OLAP answers analytics

Three systems, one data stack: OLTP serves users, ETL/ELT moves data, OLAP answers analytics Three panels left to right. OLTP (blue) handles user-facing writes at low latency on Postgres, Aurora, or Spanner, 100M to 5B transactions a day. ETL or ELT (grey, the bridge in the middle) moves and reshapes data: Spark transforms it and Kafka ships it between systems, about 100 TB a day. OLAP (violet) scans billions of rows for analytics on BigQuery, Snowflake, or Redshift, a petabyte-scale warehouse. Color key: blue is the transactional OLTP system, violet is the analytical OLAP warehouse, grey is the ETL/ELT bridge between them. Three systems, one data stack OLTP serves users, OLAP answers analytics, ETL or ELT moves data between them OLTP: user-facing writes play · like · follow · playlist < 100 ms · 1000s queries/sec single-row reads and writes Postgres ~5K TPS on one node Aurora 200K/s, Spanner 50K/region 100M–5B transactions / day ETL / ELT: move and reshape Spark transforms aggregate + enrich each day Kafka ships a durable log between systems ~100 TB processed / day OLAP: analytics scans top songs · retention · revenue 1–60 s · scan billions of rows columnar, always distributed BigQuery · Snowflake · Redshift 5 TB scan in ~15 s vs hours on one node 1 PB warehouse raw aggregates blue = transactional OLTP  ·  violet = analytical OLAP warehouse  ·  grey = the ETL/ELT bridge
One workload split across three systems. OLTP (blue) serves live writes and targets sub-100 ms latency. OLAP (violet) serves analytical scans and reads billions of rows per query. Spark reshapes each day's data and Kafka ships it between the systems. The scale and cost numbers give the order of magnitude for each layer.
Notes ↗
db-design-bigtech · ETL/ELT: Moving Data Between Worlds

ETL versus ELT: the same three steps, extract transform load, in a different order

ETL versus ELT: the same three steps, extract transform load, in a different order Two rows. Top row, ETL: Extract, then Transform (slate, the step that moves), then Load into the warehouse, so it transforms before loading. Bottom row, ELT: Extract, then Load the raw data into the lake, then Transform (slate) inside the warehouse with SQL and dbt, so it loads raw and transforms in place. The slate Transform box sits in the middle for ETL and at the end for ELT, so the two steps visibly swap. Color key: slate is the Transform step that changes position; grey is Extract and Load. ETL vs ELT: same three steps, different order The only difference is whether you transform before or after loading. ETL Extract Transform · Spark Load · warehouse Transform before load. Rigid: one pipeline owns the schema. ELT Extract Load raw · lake Transform · SQL + dbt Load raw, transform in place. Flexible: reshape on demand. slate = the Transform step (it moves)  ·  grey = Extract and Load
ETL transforms before loading. A nightly Spark job cleans and aggregates raw data, then writes finished tables into the warehouse. ELT loads raw data into cheap storage, then transforms inside the warehouse with SQL and dbt. Kafka ships the data in both approaches.
Notes ↗
M6

Data Quality & Observability

You can load raw data quickly.

4 figures
data-quality · Raw data is dirty

Raw data lies in five ways

Raw data lies in five ways Five cards, each a real way raw Spotify listen data arrives broken: a missing value (genre is NULL), a duplicate (the same listen logged twice), a wrong type (text where a timestamp belongs), a late event (arriving days out of order), and schema drift (an upstream rename). Each card shows the clean field in slate and the corrupted value in red. Color key: slate is a clean field, red is the corrupted value. Raw data lies in five ways You loaded it raw and fast. Now the mess is yours to catch. Missing genre = NULL a podcast has no genre Duplicate listen 7, 7 same play logged twice Wrong type time = "today" text where a date belongs Late 3 days late an event arrives out of order Schema drift genre → category upstream renamed the column slate = a clean field  ·  red = the corrupted value that just entered your warehouse
Raw pipelines fail in recurring ways. A value goes missing, a row duplicates, a field arrives with the wrong type, an event arrives late and out of order, or an upstream rename drifts the schema. Slate marks a clean field. Red marks a corrupted value that entered the warehouse.
Notes ↗
data-quality · Test what you can predict

A failed test stops the build before the bad number ships

A failed test stops the build before the bad number ships A four-stage pipeline in slate: raw_listens, then stg_listens, then the top_genres mart, then the dashboard, each an arrow to the next. Under staging, two declared tests run: unique(listen_id) passes in green, not_null(genre) fails in red. The failed test raises a red barrier that stops the build, so the top_genres mart and the dashboard are greyed out and never receive the wrong number. Color key: slate is a table the pipeline builds, green is a test that passed, red is a test that failed and blocks the build. Test what you can predict: a failed check stops the build Each model is a SELECT. Each test is an assertion on its output. A break never reaches the dashboard. raw_listens loaded as-is stg_listens cleaned + typed top_genres the dashboard number dashboard what the exec sees tests on stg_listens unique(listen_id) not_null(genre) 3,412 rows have a NULL genre BUILD STOPS The wrong number is never built, so it never reaches the exec. slate = a table the pipeline builds  ·  green = test passed  ·  red = test failed, build blocked
A failed test blocks the build. Each model runs as a `SELECT`. Each test asserts a property of the model output. `unique(listen_id)` passes (green). `not_null(genre)` fails (red) because thousands of podcast plays have no genre. The failure stops the pipeline, so the `top_genres` table never builds and the dashboard keeps yesterday's value. `NOT NULL` and `UNIQUE` constraints enforce the same properties at write time. dbt enforces them during transform.
Notes ↗
data-quality · Observe what you cannot predict

Observability catches the drift the top-line hides

Observability catches the drift the top-line hides Two line charts. On the left, total plays across all cities stays inside its expected grey band, so the top-line looks healthy and no test fires. On the right, the same metric split by city shows Chicago falling out of its expected band in red, where a monitor fires an alert. No assertion predicted this; a learned baseline caught it. Color key: slate is the metric line, the grey band is its expected range, red is where a slice fell out of the band and fired an alert. Observe what you cannot predict: watch each slice drift No test asserted this. A learned baseline flags the anomaly the top-line number hides. Total plays, all cities expected range looks healthy, no alert fires Same metric, split by city Chicago's expected range below baseline Chicago drifted, alert fires in 2 days, not 45 slate = the metric  ·  grey band = its expected range  ·  red = a slice out of range, alert fired
A global metric can stay inside its expected range while a slice breaks. Total plays remain within the expected band, so tests and dashboards stay quiet. A per-city split exposes Chicago falling outside its learned baseline (red), which triggers an alert. Uber built this approach in D3 and reduced detection time from forty-five days to two. Slate marks the metric. The grey band marks the expected range. Red marks the drifting slice.
Notes ↗
data-quality · Takeaway

Trust is a ladder of four defenses

Trust is a ladder of four defenses Four slate steps rising left to right, each a stronger data-quality defense that catches what the one below cannot: constraints in the database at write time, dbt tests that block the failures you can name, observability that alerts on the ones you cannot, and an orchestrator (Prefect or Airflow) that runs it all and gates on the checks. Color key: slate is a rung of the trust ladder, each higher rung catching what the lower one misses. Trust is a ladder Each rung catches what the one below cannot. more trust Constraints NOT NULL, UNIQUE, at write time Tests (dbt) block the failures you can name Observability alert on the ones you cannot Orchestration Prefect or Airflow, run and gate slate = a rung of the trust ladder, each one catching what the rung below misses
Constraints (`NOT NULL`, `UNIQUE`) enforce properties at write time. dbt tests block failures you can name before they ship. Observability alerts on failures you cannot name by detecting drift. An orchestrator runs the sequence on a schedule and gates downstream work on these checks. Slate marks a rung. Each higher rung covers cases that bypass lower rungs.
Notes ↗
M6

Optional 6.3c · Big Schemas: What ELT Actually Builds

A warehouse grows by derivation.

4 figures
big-schemas · What actually gets big

One visit, three organisations

One visit, three organisations The top row tells the real-world story left to right: a person goes in for a visit, a hospital whose providers treat them, and an insurer that pays the bill. An arrow labelled visits joins the person to the hospital, and an arrow labelled bills joins the hospital to the insurer. Below each one sits the record that organisation keeps, named by the FHIR standard: Patient, Encounter, and Claim. Two arrows run right to left between the records, showing that the Claim bills for the Encounter and the Encounter was for the Patient. A band below notes that no party can force the others to use its column names, so the industry agreed on a shared vocabulary that arrives as nested JSON and is versioned. One visit, three organisations Nobody designed this for a warehouse. It came from the business. THE PERSON A patient goes in for one visit visits THE PROVIDER A hospital its doctors treat them bills THE PAYER An insurer pays for the visit ITS RECORD Patient ITS RECORD Encounter ITS RECORD Claim bills for was for Three organisations, one visit, and none can force the others to use its column names. So the industry agreed on a shared vocabulary called FHIR. It arrives as nested JSON, and the standard is versioned.
One visit produces three records in three organisations. FHIR defines dozens more resources. Claim references an Encounter. That Encounter references a Patient. Different companies maintain each record, so references cross company boundaries. The standard changes on a schedule outside your control. The source system chose the format, and the warehouse receives it in that form.
Notes ↗
big-schemas · Everyone builds one of these

Everyone builds one of these

Everyone builds one of these A shaded template row at the top shows the shape every organisation builds: sources, then cleaned, then combined, then published. Below it two organisations run their own copy of that shape. The hospital goes from an EHR extract to cleaned visits to visits by patient to a published count of visits delivered. The insurer goes from a claims feed to cleaned claims to claims by member to a published count of visits paid for. A bracket on the right joins both published numbers into one box noting that these are the same visits counted two ways, and that a denied claim is a visit the hospital delivered and the insurer never paid for, so reconciling the gap is its own pipeline. A closing line notes that every lab, pharmacy and registry runs another one, and no single warehouse holds the whole picture. Everyone builds one of these Same shape, different organisation, and the numbers still have to agree. THE SHAPE sources cleaned combined published HOSPITAL the provider EHR extract cleaned visits visits by patient visits delivered INSURER the payer claims feed cleaned claims claims by member visits paid for The same visits, counted two ways. A denied claim is a visit the hospital delivered and the insurer never paid for. Reconciling that gap is its own pipeline, built on top of both. Every lab, pharmacy and registry runs another one of these. No single warehouse holds the whole picture.
Two organisations run the same four steps without coordination. The hospital publishes visits delivered. The insurer publishes visits paid for. Both counts describe the same visits and can disagree. A denied or still-pending claim produces a visit the hospital delivered and the insurer did not pay for. Each pipeline can still satisfy its own definition. Another pipeline can reconcile the two by reading both outputs.
Notes ↗
big-schemas · One job per stage

Eight stages, one job each

Eight stages, one job each A pipeline of eight stages flowing top to bottom, each an equal-width box joined to the next by an arrow. Each box names the stage, says what job it does, shows a real model name from that stage, and carries a count of how many models it holds. Raw lands nine feeds under FHIR's own names, staging flattens and renames them to plain business words, a first intermediate stage joins the three parties, a second costs the encounters, marts build facts, aggregates roll up to the member month, metrics name the numbers, and exposures serve them. A closing line notes that nine raw feeds become forty-two models, and that the prefix on a model name tells you which stage it belongs to. Eight stages, one job each Each stage does one job and hands its result to the next. raw nested JSON, under FHIR's own names raw_encounter 9 staging flatten, type, drop duplicates, rename to plain words stg_visit 9 intermediate 1 join the three parties: patient, provider, payer int_visit_joined ~12 intermediate 2 cost each visit against its claim lines int_visit_costed ~8 marts dimensions and facts, modelling the business fct_visits ~6 aggregates roll claims up to the member-month agg_member_month ~4 metrics the named numbers the business argues about readmission_rate_30d 3 exposures dashboard, regulatory report, ML features not a model 3 Nine raw feeds become forty-two models. The prefix is the stage: stg_ cleaned, int_ derived from other models, fct_ a fact table you query.
One build splits into stages. Raw keeps FHIR names chosen by the source. Staging flattens nested JSON, assigns types, drops duplicates, and renames columns to business terms, so raw_encounter becomes stg_visit. Intermediate joins the three parties, then prices each visit from its claim lines. Marts model business entities. Aggregates roll claims up to the member-month because per-claim granularity does not serve reporting. Metrics store named numbers. Exposures are dashboards and reports that read the metrics.
Notes ↗
big-schemas · The graph nobody drew

The graph nobody drew

The graph nobody drew Fifty-one models drawn as dots in five columns by stage: nine raw feeds, nine staging, twenty intermediate, ten marts and aggregates, and three metrics. Lines run between adjacent columns and cross heavily. One route is highlighted and named at every step, running from raw_encounter through stg_visit and int_visit_costed to fct_visits and finally the published metric readmission_rate_30d. An axis below marks that walking left is upstream, toward what a model was built from, and walking right is downstream, toward everything that depends on it. The graph nobody drew The same pipeline as above. This is the shape it actually has. RAW STAGING INTERMEDIATE MARTS + AGGREGATES METRICS raw_encounter stg_visit int_visit_costed fct_visits readmission_rate_30d upstream · what it was built from downstream · what depends on it One published number sits six models deep, inside a graph of forty-two.
A pipeline is a dependency graph of models. Each dot is a model. Each line is one model reading another. Columns group models by stage from Figure 3. The highlighted route names each step: raw_encounter lands source data, stg_visit cleans it, int_visit_costed joins parties and prices visits, fct_visits shapes the business table, and readmission_rate_30d stores a metric. One route uses six models inside a graph of forty-two.
Notes ↗
M6

Optional 6.3d · Lineage: What Broke, and What Will I Break?

The graph that 6.3c produced supports two traversals.

5 figures
lineage-blast-radius · Six models, all green

Every model green, the number still wrong

Every model green, the number still wrong Six models stacked from raw_encounter at the bottom to readmission_rate_30d at the top. Each carries a green tick because every model built successfully. An arrow walks upward from the metric through each model until it reaches int_visit_completed, marked in red, whose filter tests status equals finished while the source now sends status equals completed, so the model returns zero rows and everything above it computes on an empty set. Every model green. The number still wrong. Walk upstream until the data stops making sense. The last sensible model holds the fault. readmission_rate_30d down 30% overnight · reads like a clinical win fct_visits built successfully · all tests passed int_visit_costed built successfully · all tests passed int_visit_completed where status = 'finished' FHIR R5 renamed the value. The source now sends 'completed'. Zero rows match. stg_visit rows present and correct · the fault is above this raw_encounter landed exactly as the source sent it the walk upstream An empty result is a valid result. The count is zero, the average is undefined, and nothing errors.
Every model is green and the number is still wrong. The walk starts at the symptom and moves upstream one edge at a time until the values stop making sense. It ends at int_visit_completed, whose filter tests a status value the source no longer sends. Nothing errored, because an empty result is a valid result: the count is zero, the average is undefined, and everything above it computes on nothing. This is the failure 6.4 calls schema drift, found by walking the graph.
Notes ↗
lineage-blast-radius · One graph, two directions

One graph, two directions

One graph, two directions A single pipeline graph with one model highlighted in the centre. Above it sit its ancestors and below it sit its descendants. On the left, an upward arrow labelled plus model is titled root cause and asks why is this number wrong. On the right, a downward arrow labelled model plus is titled blast radius and asks what will I break. The same graph and the same traversal serve both, with only the arrow direction reversed. One graph, two directions The same traversal. Only the arrows are reversed. fct_visits the model you are standing on stg_visit int_visit_costed readmission_rate_30d exec dashboard quality report +model root cause why is this number wrong? model+ blast radius what will I break? Debugging and change management feel like different skills. They are the same query with the arrows reversed.
One graph, one node, two directions. Walking to the ancestors answers why is this number wrong; walking to the descendants answers what will I break. Debugging and change management are the same query here, with the arrows reversed. Both run from a terminal against the graph you already have.
Notes ↗
lineage-blast-radius · Model-level says 23. Column-level says 6.

Model-level says 23. Column-level says 6.

Model-level says 23. Column-level says 6. Two panels comparing lineage granularity for the same proposed change. The left panel, model-level, shows one source model with twenty-three downstream models drawn as an undifferentiated block, labelled true but useless. The right panel, column-level, shows the same source column with only six downstream models split into three kinds: two copy, which pass the value through and need backfilling, three transform, which compute from it so their numbers change, and one inspect, which reads it only in a filter so values are unchanged but row counts may shift. Model-level says 23. Column-level says 6. Splitting one column: stg_measurement.value_quantity MODEL-LEVEL stg_measurement everything that reads the table 23 downstream models indistinguishable from each other True, and useless. That is half the warehouse. COLUMN-LEVEL stg_measurement.value_quantity copy 2 passes through unchanged · needs a backfill transform 3 computed from · BMI, risk score, regulatory measure inspect 1 read only in a filter · row counts may shift Three different jobs. The coarse answer hid all three.
The same change, asked at two granularities. Model-level lineage sees only that a model touched the table, so it reports the whole subtree. Column-level lineage classifies each dependency: copy passes the value straight through, so an export and a feature table need backfilling; transform computes from it, so a BMI calculation, a risk score and a regulatory measure all change their numbers; inspect only reads it in a WHERE or a JOIN, so values do not change but row counts might. Three different jobs, and the coarse answer hid all three.
Notes ↗
lineage-blast-radius · Why the finer answer costs more

One edge you declared. The rest is inside the SQL.

One edge you declared. The rest is inside the SQL. A band at the top shows the one edge you declared: stg_measurement feeding fct_visits, which costs nothing to know because you wrote it. Below, a short SQL query is annotated at three lines to show why column lineage is harder. The star in a select has to be expanded before anything knows what it selected. A summed and renamed expression produces weight_lb, a name that appears nowhere upstream. A where clause changes which rows survive without changing any value. A closing line notes that model-level lineage reads your declaration while column-level lineage has to read your SQL. One edge you declared. The rest is inside the SQL. Model-level lineage reads what you wrote. Column-level has to work out what it means. stg_measurement fct_visits One edge. You wrote it, so it costs nothing to know. with m as (select * from stg_measurement) select patient_id, sum(value_quantity * 2.2) as weight_lb, count(*) as n from m where status = 'final' group by 1 Expand the star before you know what it even selected. Renamed and computed. weight_lb appears nowhere upstream. Changes which rows survive, and not one value. Model-level lineage reads your declaration. Column-level has to read your SQL.
The one edge you declared, and the three reasons the rest is work. ref() gave the tool stg_measurement feeding fct_visits for free. Everything finer is buried in the query: the star has to be expanded before anything knows what it selected, weight_lb is a name that exists only in the output, and the WHERE changes which rows survive without touching a single value. Answering what depends on this column means resolving all three.
Notes ↗
lineage-blast-radius · The agent walks it too

Guessing, or querying

Guessing, or querying One question sits at the top: if I drop value_quantity, what breaks? Two panels below answer it. Without the graph, the model predicts a plausible answer, naming models that sound like yours, some of which do not exist. With the graph, the model queries the lineage instead and returns the six models that actually depend on that column. A closing note records that Anthropic's text-to-SQL accuracy went from twenty one percent to ninety five percent on its own warehouse with no change to the model, that grounding produced that gain, and that this graph is one piece of the grounding. Guessing, or querying The same question, put to an agent twice. "If I drop value_quantity, what breaks?" WITHOUT THE GRAPH It predicts a plausible answer. Names models that sound like yours. Some of them do not exist. WITH THE GRAPH It queries the lineage instead. Returns the six models that really depend on that column. Anthropic's text-to-SQL went from 21% to 95% on its own warehouse, with no change to the model. Grounding did that. This graph is one piece of the grounding.
The same question asked twice. A model with no access to your warehouse can only produce something shaped like an answer, and a plausible list of model names is exactly the failure that is hardest to notice. With the graph the question stops being a prediction and becomes a lookup.
Notes ↗
M6

Case Study: Claude's KV Cache

An LLM agent sends a long, nearly identical prefix on every turn: system prompt, tool schemas, and conversation history.

5 figures
case-study-kv-cache · Every turn is mostly the same prefix

The KV cache across agent turns

The KV cache across agent turns Top: one request is a bar split into system prompt, tool schemas, and history (the slate prefix, reused every turn) plus a small new message that must be computed; the prefix grows each turn but is reused, not recomputed. Bottom: a cost chart over four turns. The grey bars, recompute the prefix every turn, rise turn after turn; the slate bars, prompt caching, pay once to write the cache then stay low as each turn only reads the prefix and computes the new message. Color key: slate is prompt caching; grey is recomputing the prefix every turn. The KV cache across agent turns Most of the prefix repeats every turn; cache it and reuse it instead of recomputing. One request System prompt Tool schemas Conversation history New message the prefix: reused every turn, not recomputed must compute recompute the prefix every turn prompt caching: write once, then reuse cost per turn (relative) Turn 1 Turn 2 Turn 3 Turn 4
An agent resends most of its prefix every turn, so caching it saves the most. A request is a long prefix that only grows at the end (system prompt, tool schemas, then the conversation so far) plus a short new message; without caching, the model recomputes the whole prefix each turn, so cost climbs as the conversation grows (grey). Prompt caching stores that prefix's attention state and reads it back on every later turn, so each turn computes only the new message and cost stays flat (slate). It is the same trade as keeping your working set in RAM from Module 2: a cache hit is cheap, recomputing is not.
Notes ↗
case-study-kv-cache · Every turn is mostly the same prefix

What the KV cache stores: the prefix as key, the model's intermediate results as value

What the KV cache stores: the prefix as key, the model's intermediate results as value A key-value store. The key (grey) is the prefix, a sequence of tokens (You, are, Claude, and so on). The value (slate) is the model's intermediate work from reading the prefix: for each token a key vector K and a value vector V, each drawn as a row of cells, repeated at every layer. It is the precomputed state the model would otherwise rebuild each turn, not the token text and not the output. Color key: grey is the prefix tokens (the key); slate is the K and V vectors (the value). What the KV cache stores The value is the model's intermediate work from reading the prefix, saved so it never reads the prefix twice. KEY: the prefix VALUE: the model's intermediate results You are Claude ... + tools + history K V K V K V each is a dense vector, one K and one V per token, at every layer It is the precomputed state the model built while reading the prefix, not the token text and not the output. You never read it; the model does. Store it once, keyed by the prefix, and a repeat is a lookup, not a recompute. grey = the prefix tokens (the key)  ·  slate = the K and V vectors (the value)
What the KV cache stores. The key is the prefix (its tokens); the value is the model's intermediate work from reading it, a key vector and a value vector for each token, at every layer. It is the precomputed state the model would otherwise rebuild each turn, not the token text and not the output, so caching it skips the expensive part: you reuse the work instead of running the prefix through the network again.
Notes ↗
case-study-kv-cache · Paging: fit many caches in one GPU

PagedAttention pages the KV cache into fixed-size blocks instead of reserving a chunk per request

PagedAttention pages the KV cache into fixed-size blocks instead of reserving a chunk per request R1, R2, and R3 are three concurrent chats sharing one GPU, each with a KV cache of computed keys and values that grows one token at a time. Two panels. Left, the naive way: each request reserves one contiguous chunk sized to the maximum length, but only a few cells hold real data (slate) and the rest are reserved and never used (dashed), wasting 60 to 80 percent of GPU memory. Right, PagedAttention: each request's real tokens are split into fixed-size blocks packed together with no gaps, and a page table maps each request to its blocks, which can sit anywhere, for example request one lives in blocks 0 and 2, wasting under 4 percent. Color key: slate is a block of real KV data; a dashed outline is memory reserved but never used. PagedAttention: page the cache, do not reserve it R1, R2, R3 are three chats sharing one GPU. Each stores a KV cache that grows one token at a time. Naive: reserve a chunk of GPU memory per chat R1 R2 R3 reserved to the max length, never used 60 to 80% wasted page it Paged: fixed-size blocks + a page table R1 R2 R1 R3 R3 0 1 2 3 4 Page table (request → blocks) R1 → 0, 2 R2 → 1 R3 → 3, 4 R1 sits in blocks 0 and 2, not adjacent: a block can go anywhere free. packed: under 4% wasted slate = a block of computed keys and values (not prompt text)  ·  dashed = memory reserved but never used
R1, R2, and R3 are three chats served on one GPU at once, each holding a KV cache (the keys and values the model computed, not the prompt text) that grows one token at a time. The naive way (left) gives each request one contiguous chunk of GPU memory sized to its longest possible length, so most of it is reserved and never used, wasting 60 to 80 percent of GPU memory. PagedAttention (right) splits each request into fixed-size blocks, packs them with no gaps, and keeps a page table mapping each request to its blocks, which can sit anywhere (request one is in blocks 0 and 2, not adjacent), dropping waste under 4 percent. This is the page-table half of Module 2 paging, packing one memory tier rather than moving data between disk and RAM.
Notes ↗
case-study-kv-cache · The rest is Module 2

An agent's two memories, one quantization lever

An agent's two memories, one quantization lever Two memory tiers with one shared lever between them. On the left, long-term memory is the vector or RAG store, shrunk by TurboQuant from Case Study 2.4. On the right, working memory is the KV cache of the current context, shrunk by KV-cache quantization. The slate lever in the middle, quantize (drop precision, keep the signal), feeds both, because both are dense-vector piles too large at full precision. Color key: slate is the two memory tiers and the shared quantize lever; grey is the supporting detail. An agent's two memories, one quantization lever Long-term and working memory are both dense-vector piles; the same lever shrinks both. Long-term memory the vector / RAG store billions of embeddings TurboQuant · Case Study 2.4 Working memory the KV cache of the context grows with every token KV-cache quantization Quantize drop precision, keep the signal full precision to a few bits slate = the two memory tiers and the shared quantize lever  ·  grey = supporting detail
An agent keeps two kinds of memory, and one lever shrinks both. Long-term memory is the vector store it retrieves from, kept small by TurboQuant (Case Study 2.4); working memory is the KV cache of the current context, kept small by KV-cache quantization. Both are dense-vector piles too large to hold at full precision, so both use the same move: drop precision, keep the signal.
Notes ↗
case-study-kv-cache · The rest is Module 2

Every KV-cache memory trick is a Module 2 idea

Every KV-cache memory trick is a Module 2 idea A two-column map. Each row pairs an agent KV-cache technique on the left (slate) with the Module 2 concept it reuses on the right: PagedAttention with paging, prefix and prompt caching with the cache-hit-versus-recompute cost model, KV-cache quantization with the quantization of Case Study 2.4 (TurboQuant), and GPU-to-CPU-to-disk offload with the storage hierarchy. Color key: slate is the LLM-serving technique; grey is the Module 2 concept it reuses. Every KV-cache memory trick is a Module 2 idea The frontier of LLM serving reuses the systems primitives you already learned. Agent KV-cache technique The Module 2 idea it reuses PagedAttention Paging Module 2 · Storage & Paging Prefix / prompt caching Cache hit vs recompute Module 2 · IO Cost Model KV-cache quantization Quantization Module 2 · Case Study 2.4 (TurboQuant) GPU to CPU to disk offload The storage hierarchy Module 2 · the speed gap slate = the LLM-serving technique  ·  grey = the Module 2 idea it reuses
The techniques that keep an agent's KV cache small and fast are Module 2 primitives under new names. PagedAttention breaks the KV cache into fixed-size pages addressed through a page table, so scattered GPU memory packs tight instead of demanding one contiguous chunk; it is the same fixed-size-page idea as Module 2 paging, borrowing the page-table half rather than the disk-to-RAM movement. Prefix and prompt caching turn a recompute into a cheap cache hit, priced by the same cost-model reasoning as Module 2 (here the expensive operation is compute, not a disk read). KV-cache quantization is drop-precision quantization, the same lever as TurboQuant in Case Study 2.4, and offloading the cache across GPU, CPU, and disk is the storage hierarchy. Nothing in the right column is new to you.
Notes ↗
M6

Data Security: Trust No One

Zero Trust assumes compromise at every layer: network, users, admins, code, and backups.

2 figures
data-security · Zero Trust: The Five Surfaces

Zero Trust: assume every surface between a request and the data is already hostile

Zero Trust: assume every surface between a request and the data is already hostile A grey data store at the top holds the database and its PII. Below it sit five red surfaces you must not trust by default: the network (assume compromised), users (assume phished), admins (assume insider threat), code (assume injection), and backups (assume deletable). Zero Trust means verifying every request rather than trusting any of these. Color key: grey is the data you protect, red is a surface to distrust and verify. Zero Trust: assume every surface is hostile Trust nothing by default; verify every request to the data Your database + PII every path in is a trust boundary Network assume: compromised man-in-the-middle packet sniffing DNS poisoning Users assume: phished credential theft social engineering malware on devices Admins assume: insider threat privilege abuse compromised account least privilege, RLS Code assume: injection SQL injection supply-chain attacks parameterize inputs Backups assume: deletable ransomware target untested restores 3-2-1, immutable Zero Trust: never trust a surface; verify every request, every time. grey = the data you protect  ·  red = a surface to distrust and verify
Zero Trust treats every path to the data as hostile until verified. Attackers can sniff the network, phish a user, compromise an admin account, inject code, or encrypt backups with ransomware. Defenses verify each request at each surface and apply least privilege, MFA, and immutable backups.
Notes ↗
data-security · SQL Injection: The One Bug That Keeps Winning

SQL injection: the same attacker input is a command in a concatenated query but harmless data in a parameterized one

SQL injection: the same attacker input is a command in a concatenated query but harmless data in a parameterized one The same attacker input, Robert semicolon DROP TABLE users dash dash, takes two fates. On the left (red), string concatenation splices it into the query text, so the database runs DROP TABLE users and the table is gone. On the right (green), a parameterized query binds it as a single value, so the database looks for a user literally named that whole string, finds none, and nothing is dropped. Color key: red is the injection executing as code, green is the input bound as data. SQL injection: same input, two fates input: Robert'; DROP TABLE users; -- String concatenation, vulnerable # input spliced into query text q = f"SELECT * FROM users WHERE name = '{username}'" cursor.execute(q) the database receives SELECT * FROM users WHERE name = 'Robert'; DROP TABLE users; --' ^ ends query ^ new command ^ comment input becomes a command the users table is dropped Parameterized query, safe # input passed separately, not spliced q = "SELECT * FROM users WHERE name = %s" cursor.execute(q, (username,)) the database receives SELECT * FROM users WHERE name = 'Robert''; DROP TABLE users; --' ^ the whole string is one value input stays data no user matches; nothing is dropped red = the injection runs as code  ·  green = the input bound as data
The same input can act as SQL or as data. String concatenation lets an attacker terminate a string literal and inject a new command, so DROP TABLE users executes. A parameterized query sends values separately from SQL text, so the database treats the input as one value and executes no injected command. Parameterize every query and never concatenate input into SQL.
Notes ↗
M6

Data Privacy: The Netflix Challenge

"Anonymized" data rarely stays anonymous.

1 figure
data-privacy · Anonymous Is Not Private

The linkage attack: joining anonymized Netflix data to public IMDB reviews re-identifies a user

The linkage attack: joining anonymized Netflix data to public IMDB reviews re-identifies a user Two grey datasets sit side by side. The Netflix release is anonymized: user A7X9B2 rated Shrek and Matrix on specific dates. Public IMDB reviews carry the same rare movie, rating, and date pairs under the real name Alice Smith. Joining the two on those shared pairs yields a red result: A7X9B2 equals Alice Smith, exposing her whole history. Color key: grey is the two datasets, red is the re-identification. Anonymous is not private: the linkage attack Netflix Prize, 2006: 100M ratings from 500K "anonymous" users. Re-identified in 2007. Netflix release, "anonymized" user: A7X9B2 Shrek rated 4 on 2006-01-15 Matrix rated 5 on 2006-01-20 Titanic rated 2 on 2006-02-03 IMDB, public reviews user: Alice Smith Shrek rated 4 on 2006-01-15 Matrix rated 5 on 2006-01-20 join on (movie, rating, date) A7X9B2 = Alice Smith her entire viewing history is now de-anonymized A few rare ratings with dates are a fingerprint: about 8 are enough to be ~99% unique. grey = the two datasets, each "safe" alone  ·  red = the re-identification the join exposes
A join on (movie, rating, date) can map an anonymous ID to a real name. A user's ratings form a rare pattern across movies, scores, and dates. The same user can publish some of those ratings on IMDB under a real name. The join links the random Netflix ID to that name and reveals the rest of the Netflix history under the same ID.
Notes ↗
M6

Case Study: Google COVID Reports (Differential Privacy)

Removing names does not make data anonymous.

1 figure
case-study-privacy

Differential privacy turns an exact count into a safe, noisy one

Differential privacy turns an exact count into a safe, noisy one Three panels left to right. 1, Exact query: a SQL COUNT of visits per location returns the true count, 100 visits at the testing center Bob went to, which is identifying. 2, Add noise (slate): differential privacy adds calibrated Laplace noise tuned by a privacy budget epsilon of 1, scaled to mask any single person. 3, Released count (slate): the published number comes out 98, or 103, or 101, a little different each run, so the trend stays true but Bob's one visit can never move the answer. Color key: slate is the differential-privacy step and its noisy result; grey is the exact query and true count. Differential privacy: the same query, a safe answer Add calibrated noise, so the trend stays true but no single person shows. 1 · Exact query SELECT location_id, COUNT(user_id) FROM visits GROUP BY location_id 100 true count · identifies Bob 2 · Add noise + Laplace noise ε = 1 privacy budget scaled to mask any one person 3 · Released count 98 · 103 · 101 three runs, three answers the trend holds; Bob stays hidden slate = the differential-privacy step and its noisy result  ·  grey = the exact query and true count
Differential privacy protects the released report. The exact query returns a true count (say 100 visits at the center Bob went to), which on its own can identify him. Differential privacy adds calibrated Laplace noise, sized by a privacy budget (ε = 1), so the published number comes back a little different each run (98, 103, 101). The trend across locations stays accurate, but Bob's single visit can never move the answer.
Notes ↗
end of lecture 1~64 min of figures
M6 Modern data systems 1 of 34
Notes ↗ Video ▶ Why ◎ Schedule ↗ □ keys

M6 Modern data systems

Big-tech patterns, NoSQL, agents, privacy · 34 figures · ~64 min · 1 lectures at this pace