<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Gowtham Potureddi</title>
    <description>The latest articles on DEV Community by Gowtham Potureddi (@gowthampotureddi).</description>
    <link>https://dev.to/gowthampotureddi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3874592%2Fb901f929-0a60-4dd2-9dac-22ce22291bdc.png</url>
      <title>DEV Community: Gowtham Potureddi</title>
      <link>https://dev.to/gowthampotureddi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gowthampotureddi"/>
    <language>en</language>
    <item>
      <title>SQL MERGE / UPSERT / ON CONFLICT: Dialect-Aware Idempotent Writes</title>
      <dc:creator>Gowtham Potureddi</dc:creator>
      <pubDate>Sun, 12 Jul 2026 12:12:53 +0000</pubDate>
      <link>https://dev.to/gowthampotureddi/sql-merge-upsert-on-conflict-dialect-aware-idempotent-writes-1io7</link>
      <guid>https://dev.to/gowthampotureddi/sql-merge-upsert-on-conflict-dialect-aware-idempotent-writes-1io7</guid>
      <description>&lt;p&gt;&lt;strong&gt;&lt;code&gt;sql merge upsert&lt;/code&gt;&lt;/strong&gt; is the primitive every incremental load pipeline, every CDC apply-loop, every API upsert endpoint, and every dbt incremental model eventually reaches for — and it is the primitive most engineers ship with a race condition on day one because the honest "insert-if-new-else-update" contract is deceptively subtle and the dialect story is genuinely fragmented across eight engines that all spell the same idea differently. Every backend intern types &lt;code&gt;SELECT ... IF NOT EXISTS THEN INSERT ELSE UPDATE&lt;/code&gt; in week one and only discovers the race between the SELECT and the INSERT when two concurrent workers produce a primary-key violation at 3 a.m. This guide is the honest, dialect-aware tour of what actually happens when you ask an engine to atomically merge new rows into an existing table without losing writes and without producing duplicates.&lt;/p&gt;

&lt;p&gt;The tour walks the eight engines you have to keep straight in 2026 — the ANSI SQL:2003 &lt;code&gt;MERGE INTO ... USING ... ON ... WHEN MATCHED / WHEN NOT MATCHED / WHEN NOT MATCHED BY SOURCE&lt;/code&gt; grammar and its three row-level branches, the OLTP-flavoured &lt;code&gt;INSERT ... ON CONFLICT (col) DO UPDATE SET col = EXCLUDED.col&lt;/code&gt; (Postgres, SQLite) and &lt;code&gt;INSERT ... ON DUPLICATE KEY UPDATE ... = new.col&lt;/code&gt; (MySQL) native UPSERTs that guarantee atomicity via unique-index detection instead of a two-statement dance, the warehouse-flavoured &lt;code&gt;MERGE INTO t USING s ON ...&lt;/code&gt; on Snowflake, BigQuery, and Databricks with their per-engine storage-rewrite models (micro-partition rewrites on Snowflake, partition rewrites on BigQuery, Delta file rewrites on Databricks), the notorious SQL Server MERGE cardinality bug and the safer alternatives senior engineers use in its place, and the anti-patterns that turn any of the above into a lost-update incident under concurrency. Every section ships a teaching block followed by a Solution-Tail interview answer — code, a step-by-step trace, an output table, then a concept-by-concept breakdown of why it works — so you leave with the two-line skeleton &lt;em&gt;and&lt;/em&gt; the reason it wins.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2rtajgog0jygsq41kfcd.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2rtajgog0jygsq41kfcd.jpeg" alt="PipeCode blog header for SQL MERGE / UPSERT / ON CONFLICT deep dive — bold white headline 'MERGE / UPSERT' with subtitle 'Dialect-Aware Idempotent Writes' and a stylised scene showing a target table merging with a source stream via WHEN MATCHED and WHEN NOT MATCHED branches on a dark gradient with purple, green, orange, and blue accents and a small pipecode.ai attribution." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you want &lt;strong&gt;hands-on reps&lt;/strong&gt; immediately after reading, drill the &lt;a href="https://pipecode.ai/explore/practice/topic/optimization" rel="noopener noreferrer"&gt;SQL optimization practice library →&lt;/a&gt; for merge-cost tuning, rehearse the &lt;a href="https://pipecode.ai/explore/practice/topic/joins" rel="noopener noreferrer"&gt;SQL join drill room →&lt;/a&gt; since MERGE is a specialised join, and sharpen the broader &lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;SQL practice surface →&lt;/a&gt; that covers 450+ DE-focused problems on incremental loads, idempotency, and upsert patterns.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;On this page&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why idempotent writes matter in 2026&lt;/li&gt;
&lt;li&gt;SQL Standard MERGE anatomy&lt;/li&gt;
&lt;li&gt;Postgres ON CONFLICT + MySQL ON DUPLICATE KEY + SQLite UPSERT&lt;/li&gt;
&lt;li&gt;Snowflake + BigQuery + Databricks MERGE&lt;/li&gt;
&lt;li&gt;Anti-patterns and dialect matrix&lt;/li&gt;
&lt;li&gt;Cheat sheet — MERGE / UPSERT recipe list&lt;/li&gt;
&lt;li&gt;Frequently asked questions&lt;/li&gt;
&lt;li&gt;Practice on PipeCode&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1. Why idempotent writes matter in 2026
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The &lt;code&gt;sql merge upsert&lt;/code&gt; mental model — idempotency as a contract, the two loading anti-patterns, and where MERGE shows up in every real DE pipeline
&lt;/h3&gt;

&lt;p&gt;The one-sentence invariant: &lt;strong&gt;an idempotent write is a statement that, when replayed any number of times against the same target state and the same input, produces the same final state — no duplicates, no lost updates, no partial writes — and MERGE / UPSERT / ON CONFLICT are the SQL primitives that let you express this contract atomically in a single round-trip instead of a two-statement dance that races under concurrency&lt;/strong&gt;. Every pipeline eventually retries; only pipelines built on idempotent primitives survive that retry without corruption.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where MERGE / UPSERT actually shows up in production.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;dbt incremental models.&lt;/strong&gt; Every &lt;code&gt;materialized='incremental'&lt;/code&gt; model with &lt;code&gt;unique_key='id'&lt;/code&gt; compiles to a &lt;code&gt;MERGE&lt;/code&gt; on Snowflake / BigQuery / Databricks and to an &lt;code&gt;INSERT ... ON CONFLICT ... DO UPDATE&lt;/code&gt; on Postgres. The &lt;code&gt;unique_key&lt;/code&gt; config &lt;em&gt;is&lt;/em&gt; the ON condition; the entire dbt incremental pattern is one big MERGE.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CDC apply-loops.&lt;/strong&gt; Debezium streams inserts, updates, and deletes from Postgres → Kafka → Snowflake. The apply job reads a batch of change events and MERGEs them into the target table. Without MERGE, you'd have to run separate INSERT / UPDATE / DELETE statements per row-op and risk partial application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event ingestion.&lt;/strong&gt; IoT sensor readings, product analytics events, ML feature updates — all arrive as append-only streams that need to upsert by &lt;code&gt;(entity_id, event_ts)&lt;/code&gt; when late-arriving events overwrite earlier snapshots.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API upsert endpoints.&lt;/strong&gt; REST &lt;code&gt;PUT /users/:id&lt;/code&gt; is semantically an upsert — create if new, update if existing. Backend code that does &lt;code&gt;SELECT ... IF NOT EXISTS INSERT ELSE UPDATE&lt;/code&gt; has a race window; the correct implementation is a single &lt;code&gt;INSERT ... ON CONFLICT ... DO UPDATE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dimension SCD updates.&lt;/strong&gt; Slowly Changing Dimension Type 1 (overwrite) is a simple MERGE. SCD Type 2 (history-preserving) is a MERGE with an expiration-date trick — insert a new row, close the old one via &lt;code&gt;WHEN MATCHED THEN UPDATE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fact-table backfills.&lt;/strong&gt; Reload the last 24 h of clickstream from source. MERGE by &lt;code&gt;(event_id)&lt;/code&gt; so re-runs are idempotent even when the same event flows through the pipeline twice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Change data feeds.&lt;/strong&gt; Delta Lake &lt;code&gt;CHANGES&lt;/code&gt; feed, Snowflake Streams — both consume via MERGE downstream, since the produced feed contains inserts and updates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The two loading anti-patterns.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Anti-pattern 1 — SELECT-then-INSERT / UPDATE.&lt;/strong&gt; The junior instinct: &lt;code&gt;SELECT ... IF NOT EXISTS INSERT ... ELSE UPDATE ...&lt;/code&gt;. This has a &lt;em&gt;race window&lt;/em&gt; between the SELECT and the write — two concurrent workers can both see the row as missing, both attempt the INSERT, and one gets a primary-key violation. On some engines, the race is even more subtle: one INSERT wins, the other returns "success" but silently drops the update semantics. Every senior review flags this the moment it appears.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anti-pattern 2 — blind INSERT with error swallowing.&lt;/strong&gt; &lt;code&gt;INSERT ...; IF error THEN UPDATE&lt;/code&gt;. This works but leaves an audit-log stink (rejected INSERT logs on every duplicate), thrashes the write-ahead log, and fails on some engines that don't cleanly recover from constraint violations mid-transaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anti-pattern 3 — DELETE + INSERT.&lt;/strong&gt; The "just delete the row and re-insert" pattern. Simple, but breaks referential integrity (child tables with foreign keys), triggers on both DELETE and INSERT (double effects), and violates the "no gaps in AUTO_INCREMENT" invariant many downstream systems rely on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anti-pattern 4 — application-side transactions with pessimistic locks.&lt;/strong&gt; &lt;code&gt;BEGIN; SELECT FOR UPDATE; if row exists UPDATE else INSERT; COMMIT&lt;/code&gt;. Works, but takes an exclusive row-lock that serializes writers and creates a hotspot. Fine for low volume; disastrous at 10 k QPS.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Idempotency as a contract in five bullets.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Same input → same final state.&lt;/strong&gt; Regardless of how many times you replay the same source batch, the target ends up in the same state. &lt;code&gt;run 5 times&lt;/code&gt; = &lt;code&gt;run 1 time&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No visible intermediate states.&lt;/strong&gt; During the MERGE, the target is either fully-old or fully-new to any concurrent reader. Isolation-level dependent, but MERGE is atomic per-row on most engines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No lost updates under retry.&lt;/strong&gt; If a worker crashes mid-batch and the batch is re-driven, no rows are lost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No duplicates under retry.&lt;/strong&gt; No rows appear twice; the unique-index / MERGE ON condition guarantees this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic under concurrency.&lt;/strong&gt; Two concurrent MERGEs of the same source batch produce identical target states (though they may serialize / block each other during execution).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What senior interviewers actually probe when they open a MERGE question.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Do you know the difference between MERGE and UPSERT?&lt;/strong&gt; MERGE is the ANSI SQL statement with three branches (MATCHED / NOT MATCHED [BY TARGET | BY SOURCE]). UPSERT is the informal term for insert-or-update; on Postgres / MySQL / SQLite it's typically spelled &lt;code&gt;INSERT ... ON CONFLICT&lt;/code&gt; or &lt;code&gt;ON DUPLICATE KEY UPDATE&lt;/code&gt;. MERGE is a superset; UPSERT is the common two-branch case.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do you know the race condition in a naive UPSERT?&lt;/strong&gt; &lt;code&gt;SELECT then INSERT&lt;/code&gt; has a TOCTOU (time-of-check to time-of-use) race. Fix is atomic UPSERT.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do you know the SQL Server MERGE bug?&lt;/strong&gt; SQL Server's MERGE has a known cardinality bug where multiple source rows matching the same target row cause silent data loss. Microsoft's own docs recommend workaround patterns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do you know the storage cost per engine?&lt;/strong&gt; Snowflake MERGE rewrites micro-partitions (cost per partition rewritten). BigQuery MERGE rewrites partitions (cost per partition rewritten + slot_ms). Delta MERGE rewrites Parquet files (cost per file rewritten + Photon acceleration if enabled).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do you know how MERGE composes with SCD?&lt;/strong&gt; SCD Type 1 = MERGE with &lt;code&gt;WHEN MATCHED THEN UPDATE&lt;/code&gt;. SCD Type 2 = MERGE with &lt;code&gt;WHEN MATCHED THEN UPDATE ...&lt;/code&gt; on the expiration row plus a fresh INSERT for the new version. Snowflake's &lt;code&gt;MERGE ... WHEN MATCHED AND cond THEN INSERT&lt;/code&gt; is the modern spelling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do you know the race-safe API upsert pattern?&lt;/strong&gt; REST &lt;code&gt;PUT /users/:id&lt;/code&gt; handler that does a single &lt;code&gt;INSERT ... ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name RETURNING id&lt;/code&gt;. Atomic, idempotent, no application-side check.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Worked example — the race condition in the naive SELECT-then-INSERT pattern
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Two backend workers both receive a &lt;code&gt;PUT /users/42&lt;/code&gt; request at the same millisecond. Both check "does user 42 exist?" — both get NO, because the row doesn't exist yet. Both attempt the INSERT. Under READ COMMITTED isolation, one wins and the other gets a UNIQUE-constraint violation. Under snapshot isolation, both may commit and one silently loses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given the endpoint below, describe the race window and show the fixed atomic version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# The naive PUT handler
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;put_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT id FROM users WHERE id = %s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UPDATE users SET name = %s WHERE id = %s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO users (id, name) VALUES (%s, %s)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# The atomic fix — single-statement UPSERT
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;put_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        INSERT INTO users (id, name) VALUES (%s, %s)
        ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Two concurrent workers both call &lt;code&gt;put_user(42, 'alice')&lt;/code&gt; and &lt;code&gt;put_user(42, 'bob')&lt;/code&gt; at the same time.&lt;/li&gt;
&lt;li&gt;Naive path — both open a transaction. Both run &lt;code&gt;SELECT id FROM users WHERE id = 42&lt;/code&gt;. Under READ COMMITTED, both see no row (before either INSERT commits). Both branches choose INSERT. Both attempt &lt;code&gt;INSERT INTO users (id, name) VALUES (42, ...)&lt;/code&gt;. The one that commits first wins; the other hits a UNIQUE-constraint violation on the primary key.&lt;/li&gt;
&lt;li&gt;Result — one worker's request fails with a database error. The client sees a 500 and retries; the row is now visible; the retry does UPDATE. Semantically correct, but with a spurious error and log noise.&lt;/li&gt;
&lt;li&gt;Atomic path — a single &lt;code&gt;INSERT ... ON CONFLICT (id) DO UPDATE&lt;/code&gt; statement. Postgres detects the unique-index conflict &lt;em&gt;inside&lt;/em&gt; the write, atomically routes to UPDATE, and commits. No race window, no error, no retry.&lt;/li&gt;
&lt;li&gt;Rule — every UPSERT must be a single statement. Never two.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Worker&lt;/th&gt;
&lt;th&gt;Naive (race)&lt;/th&gt;
&lt;th&gt;Atomic (fix)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Worker A&lt;/td&gt;
&lt;td&gt;INSERT (42, 'alice')&lt;/td&gt;
&lt;td&gt;INSERT ... ON CONFLICT (42, 'alice') → INSERT wins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worker B&lt;/td&gt;
&lt;td&gt;INSERT (42, 'bob') → UNIQUE violation&lt;/td&gt;
&lt;td&gt;INSERT ... ON CONFLICT (42, 'bob') → UPDATE branch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client A sees&lt;/td&gt;
&lt;td&gt;201 Created (alice wins first)&lt;/td&gt;
&lt;td&gt;201 Created&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client B sees&lt;/td&gt;
&lt;td&gt;500 Internal Server Error&lt;/td&gt;
&lt;td&gt;200 OK (bob overrides alice)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Final row&lt;/td&gt;
&lt;td&gt;(42, 'alice' or 'bob') non-deterministic&lt;/td&gt;
&lt;td&gt;(42, 'alice' or 'bob') non-deterministic but idempotent&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Never write &lt;code&gt;SELECT then INSERT&lt;/code&gt;. Every backend UPSERT is one statement. Snapshot the source, ship a single &lt;code&gt;INSERT ... ON CONFLICT&lt;/code&gt; (Postgres), &lt;code&gt;ON DUPLICATE KEY UPDATE&lt;/code&gt; (MySQL), or &lt;code&gt;MERGE INTO ... WHEN NOT MATCHED / MATCHED&lt;/code&gt; (warehouses).&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — idempotent replay of a Kafka batch
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A common CDC pattern: consume a Kafka topic of user-profile change events, apply to a target table. If the consumer crashes mid-batch and re-drives the same offsets, the same events flow through again. The apply logic must be idempotent — replaying the batch produces the same final state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given the Kafka batch below with three user-profile events, show the idempotent MERGE statement that produces the same final state whether the batch runs once or five times.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"op"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"u"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"alice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"updated_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-12T10:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"op"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"u"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;43&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bob"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nl"&gt;"updated_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-12T10:01:00Z"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"op"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"u"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"alice-updated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"updated_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-12T10:02:00Z"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Stage the batch as a temp table&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TEMP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'alice'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="s1"&gt;'2026-07-12 10:00:00Z'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;43&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'bob'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="s1"&gt;'2026-07-12 10:01:00Z'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'alice-updated'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2026-07-12 10:02:00Z'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Idempotent MERGE&lt;/span&gt;
&lt;span class="n"&gt;MERGE&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt;
  &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt;
  &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Kafka batch has two events for &lt;code&gt;id=42&lt;/code&gt; — the later one (10:02) should win. Naive replay would apply both events in order; a re-drive after crash could apply them again but must not corrupt state.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;ROW_NUMBER() OVER (PARTITION BY id ORDER BY updated_at DESC)&lt;/code&gt; deduplicates within the batch — for &lt;code&gt;id=42&lt;/code&gt;, only the latest event survives. Same batch → same deduped source → same MERGE result.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WHEN MATCHED AND b.updated_at &amp;gt; u.updated_at&lt;/code&gt; — guards against out-of-order arrivals. If a stale event arrives after a fresh one, it's not applied.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WHEN NOT MATCHED THEN INSERT&lt;/code&gt; — new IDs are inserted.&lt;/li&gt;
&lt;li&gt;Replay safety — running the MERGE twice on the same batch is a no-op the second time because &lt;code&gt;b.updated_at &amp;gt; u.updated_at&lt;/code&gt; is now false (the first run already updated &lt;code&gt;u.updated_at&lt;/code&gt; to the batch's max).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Run&lt;/th&gt;
&lt;th&gt;id=42 name / updated_at&lt;/th&gt;
&lt;th&gt;id=43 name / updated_at&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Empty target&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;After run 1&lt;/td&gt;
&lt;td&gt;alice-updated / 10:02&lt;/td&gt;
&lt;td&gt;bob / 10:01&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;After run 2 (replay)&lt;/td&gt;
&lt;td&gt;alice-updated / 10:02&lt;/td&gt;
&lt;td&gt;bob / 10:01&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;After run 5 (replay)&lt;/td&gt;
&lt;td&gt;alice-updated / 10:02&lt;/td&gt;
&lt;td&gt;bob / 10:01&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Every CDC / event-stream apply-loop needs three properties in the MERGE — batch-level dedup, timestamp guard on WHEN MATCHED, and monotonic &lt;code&gt;updated_at&lt;/code&gt; in the source. Together they make replay a no-op.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — a dbt incremental model that compiles to a MERGE
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; dbt is the most common surface for MERGE in modern DE. &lt;code&gt;materialized='incremental'&lt;/code&gt; + &lt;code&gt;unique_key='id'&lt;/code&gt; config compiles to an engine-specific MERGE. The template is worth memorising — every incremental model looks like this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write a dbt model that incrementally loads recent orders into the target table, using MERGE semantics with &lt;code&gt;id&lt;/code&gt; as the merge key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- models/incremental_orders.sql (dbt)&lt;/span&gt;
&lt;span class="p"&gt;{{&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;materialized&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'incremental'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;unique_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;incremental_strategy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'merge'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;on_schema_change&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'append_new_columns'&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}}&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;source&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'raw'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'orders'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="n"&gt;is_incremental&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt; &lt;span class="p"&gt;}})&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;endif&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt; (dbt compiles this to different SQL per engine.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Postgres compile output&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;incremental_orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;incremental_orders&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt;
  &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Snowflake compile output&lt;/span&gt;
&lt;span class="n"&gt;MERGE&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;incremental_orders&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;incremental_orders&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt;
  &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- BigQuery compile output&lt;/span&gt;
&lt;span class="n"&gt;MERGE&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="nv"&gt;`project.analytics.incremental_orders`&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(...)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;TARGET&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="p"&gt;(...)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(...);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;is_incremental()&lt;/code&gt; — dbt macro that returns &lt;code&gt;true&lt;/code&gt; on subsequent runs (after the initial full-refresh). On first run, dbt does &lt;code&gt;CREATE TABLE AS SELECT ...&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WHERE updated_at &amp;gt; (SELECT MAX(updated_at) FROM {{ this }})&lt;/code&gt; — the incremental filter. Only pulls source rows newer than the highwater mark on the target.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;unique_key='id'&lt;/code&gt; — the ON condition for MERGE. dbt generates the appropriate WHEN MATCHED / WHEN NOT MATCHED branches.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;on_schema_change='append_new_columns'&lt;/code&gt; — dbt handles schema drift by appending new columns to the target when they appear in the source.&lt;/li&gt;
&lt;li&gt;Every engine compiles to the same conceptual MERGE, but the syntax differs — Postgres uses &lt;code&gt;INSERT ... ON CONFLICT&lt;/code&gt;, warehouses use &lt;code&gt;MERGE INTO&lt;/code&gt;. dbt hides the difference behind the same model config.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;th&gt;Statement type&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Postgres&lt;/td&gt;
&lt;td&gt;&lt;code&gt;INSERT ... ON CONFLICT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Atomic UPSERT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snowflake&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MERGE INTO&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Micro-partition rewrite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BigQuery&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MERGE INTO ... WHEN NOT MATCHED BY TARGET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Partition rewrite; &lt;code&gt;--dry_run&lt;/code&gt; first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Databricks&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;MERGE INTO&lt;/code&gt; (Delta)&lt;/td&gt;
&lt;td&gt;Parquet file rewrite; Photon-accelerated&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; dbt's incremental model is the canonical MERGE surface. Every DE codebase has hundreds of these; learn the compile output on your target engine so you can debug the compiled SQL when performance regresses.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Common beginner mistakes&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Writing &lt;code&gt;SELECT then INSERT&lt;/code&gt; instead of a single-statement UPSERT.&lt;/li&gt;
&lt;li&gt;Forgetting the &lt;code&gt;WHERE updated_at &amp;gt; highwater&lt;/code&gt; incremental filter — every run reads the full source.&lt;/li&gt;
&lt;li&gt;Forgetting the &lt;code&gt;WHEN MATCHED AND b.updated_at &amp;gt; u.updated_at&lt;/code&gt; timestamp guard — out-of-order events overwrite fresh ones.&lt;/li&gt;
&lt;li&gt;Confusing MERGE with UPSERT — MERGE is ANSI three-branch; UPSERT is the two-branch native pattern on OLTP engines.&lt;/li&gt;
&lt;li&gt;Assuming MERGE is atomic across concurrent writers — most engines still require row-level locking to serialize.&lt;/li&gt;
&lt;li&gt;Using MERGE for high-QPS API upserts — MERGE is designed for batch, not for 10 k QPS. Use &lt;code&gt;ON CONFLICT&lt;/code&gt; on OLTP engines instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;sql merge upsert&lt;/code&gt; interview question on choosing between MERGE and native UPSERT
&lt;/h3&gt;

&lt;p&gt;A senior interviewer often opens with: "You're building the API layer for a two-billion-row &lt;code&gt;users&lt;/code&gt; table. Every profile update from the mobile app hits a &lt;code&gt;PUT /users/:id&lt;/code&gt; endpoint. Do you use MERGE or &lt;code&gt;INSERT ... ON CONFLICT&lt;/code&gt;, and why? Then design the equivalent daily-batch backfill from a Kafka topic into the analytics warehouse — same question there."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using dual-endpoint architecture — &lt;code&gt;INSERT ... ON CONFLICT&lt;/code&gt; for OLTP, &lt;code&gt;MERGE INTO&lt;/code&gt; for warehouse
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ===================================================================&lt;/span&gt;
&lt;span class="c1"&gt;-- OLTP endpoint: high QPS, single-row UPSERT on Postgres 15&lt;/span&gt;
&lt;span class="c1"&gt;-- ===================================================================&lt;/span&gt;
&lt;span class="c1"&gt;-- API handler: PUT /users/:id&lt;/span&gt;
&lt;span class="c1"&gt;-- Expected QPS: 10,000+&lt;/span&gt;
&lt;span class="c1"&gt;-- Payload: {name, email, updated_at}&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt;   &lt;span class="c1"&gt;-- guard against stale writes&lt;/span&gt;
&lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- ===================================================================&lt;/span&gt;
&lt;span class="c1"&gt;-- Warehouse backfill: daily batch, ~50M rows, Snowflake&lt;/span&gt;
&lt;span class="c1"&gt;-- ===================================================================&lt;/span&gt;
&lt;span class="c1"&gt;-- 1. Load source stage from S3 → Snowflake table s_users_delta&lt;/span&gt;
&lt;span class="c1"&gt;-- 2. Dedup within the batch, apply MERGE&lt;/span&gt;
&lt;span class="n"&gt;MERGE&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dim_users&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s_users_delta&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;_kafka_ts&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;start_ts&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;end_ts&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt;
  &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt;
  &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;OLTP &lt;code&gt;INSERT ... ON CONFLICT&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;Warehouse &lt;code&gt;MERGE INTO&lt;/code&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;client sends &lt;code&gt;PUT /users/42&lt;/code&gt; with new name&lt;/td&gt;
&lt;td&gt;daily batch: ~50M rows land in stage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;single-row statement, atomic on unique index&lt;/td&gt;
&lt;td&gt;dedup batch by (id, updated_at DESC) — one row per id&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Postgres detects conflict → routes to UPDATE branch&lt;/td&gt;
&lt;td&gt;Snowflake plans MERGE — scans stage, matches on t.id&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;RETURNING clause gives the client the persisted row&lt;/td&gt;
&lt;td&gt;WHEN MATCHED AND updated_at &amp;gt; → UPDATE; WHEN NOT MATCHED → INSERT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;commit: single-row lock held briefly&lt;/td&gt;
&lt;td&gt;micro-partitions rewrite; wall clock ~2 min on Medium WH&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;client sees 200 in ~1-2 ms&lt;/td&gt;
&lt;td&gt;orchestrator sees success; audit log has row_count&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;O(log n) per call (unique index seek)&lt;/td&gt;
&lt;td&gt;O(source + target_matched) per batch&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The OLTP endpoint uses &lt;code&gt;ON CONFLICT&lt;/code&gt; because the workload is single-row, high-QPS, and needs atomic conflict detection at the unique index. The warehouse backfill uses &lt;code&gt;MERGE INTO&lt;/code&gt; because the workload is batch, tens of millions of rows, and benefits from the source-scan-once execution the MERGE grammar guarantees. Two different primitives, same idempotency contract.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Cost model&lt;/th&gt;
&lt;th&gt;Latency per call/batch&lt;/th&gt;
&lt;th&gt;Idempotency guarantee&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OLTP &lt;code&gt;ON CONFLICT&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;~50 μs CPU + 1-2 ms wall clock&lt;/td&gt;
&lt;td&gt;1-2 ms&lt;/td&gt;
&lt;td&gt;Atomic on unique index&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warehouse &lt;code&gt;MERGE INTO&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;~500 credits (Medium WH × 2 min)&lt;/td&gt;
&lt;td&gt;~2 min&lt;/td&gt;
&lt;td&gt;Deterministic on batch-dedup + timestamp guard&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Dual primitive for dual workload&lt;/strong&gt;&lt;/strong&gt; — OLTP is high-QPS single-row; native &lt;code&gt;ON CONFLICT&lt;/code&gt; is designed for exactly that shape (atomic conflict detection, minimal locking, fast). Warehouse batch is 50 M rows; &lt;code&gt;MERGE INTO&lt;/code&gt; scans the source once, plans MATCHED / NOT MATCHED branches, and rewrites storage in a single pass.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;&lt;code&gt;WHERE users.updated_at &amp;lt; EXCLUDED.updated_at&lt;/code&gt; guard&lt;/strong&gt;&lt;/strong&gt; — protects against stale writes overtaking fresh writes when two concurrent clients update the same row with different timestamps. The write with the older &lt;code&gt;updated_at&lt;/code&gt; becomes a no-op.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;&lt;code&gt;ROW_NUMBER() OVER (PARTITION BY id ORDER BY updated_at DESC)&lt;/code&gt; batch dedup&lt;/strong&gt;&lt;/strong&gt; — the Kafka source may have 5 events for the same user in one batch. The dedup keeps only the latest per id; the MERGE never sees duplicates in the source, avoiding the SQL Server MERGE cardinality bug (see H2-5) and simplifying the WHEN MATCHED logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;&lt;code&gt;WHEN MATCHED AND s.updated_at &amp;gt; t.updated_at&lt;/code&gt;&lt;/strong&gt;&lt;/strong&gt; — out-of-order events across batches are guarded. If a stale batch arrives after a fresh one, MATCHED rows fail the condition and the branch is a no-op.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — OLTP per-call is &lt;code&gt;O(log n)&lt;/code&gt; (unique index seek) with a fixed ~1-2 ms overhead. Warehouse per-batch is &lt;code&gt;O(source_scan + target_matched_partitions_rewrite)&lt;/code&gt;; a 50 M-row batch on Snowflake Medium is roughly 2 minutes and ~500 credits. Total daily cost is bounded by batch cadence.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — optimization&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL optimization drills&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/optimization" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — indexing&lt;/span&gt;
&lt;strong&gt;Unique index and constraint drills&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/indexing" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  2. SQL Standard MERGE anatomy
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;sql standard merge&lt;/code&gt; — the ANSI SQL:2003 grammar, the three row-level branches, and how &lt;code&gt;WHEN NOT MATCHED BY SOURCE&lt;/code&gt; extends the two-branch UPSERT into a three-way sync
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;the ANSI MERGE statement is a single-round-trip three-way sync operator that walks the source table once, joins each source row against the target via the ON condition, and applies exactly one of three actions per row — UPDATE (when a target match exists), INSERT (when the source row has no target match), or DELETE (when a target row has no source match) — turning the merge of two tables into a deterministic, atomic bulk operation&lt;/strong&gt;. Master the grammar and you can express incremental loads, dimension updates, CDC apply-loops, and full three-way syncs in a single statement per pipeline.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fusk7i922wmilnmbu88tz.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fusk7i922wmilnmbu88tz.jpeg" alt="Visual diagram of the SQL standard MERGE anatomy — a MERGE statement broken into MERGE INTO target / USING source / ON condition / WHEN MATCHED / WHEN NOT MATCHED / WHEN NOT MATCHED BY SOURCE branches, with example row-level actions per branch and a small pipecode.ai attribution." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Slot 1 — the six clauses of a canonical MERGE.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;MERGE INTO target_table [AS t]&lt;/code&gt;&lt;/strong&gt; — the write destination. Must be a base table (not a view on most engines, though some allow updatable views).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;USING source_table [AS s]&lt;/code&gt;&lt;/strong&gt; — the source. Can be a base table, subquery, CTE, or table function. Warehouses often materialise this as a temp table if complex.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ON t.pk = s.pk&lt;/code&gt;&lt;/strong&gt; — the match condition. Determines which target rows are "matched" by each source row. Usually equality on the natural or synthetic primary key; can be composite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;WHEN MATCHED [AND condition] THEN UPDATE SET col = s.col, ...&lt;/code&gt;&lt;/strong&gt; — the UPDATE branch. Optional &lt;code&gt;AND condition&lt;/code&gt; scopes which matched rows this branch applies to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;WHEN NOT MATCHED [BY TARGET] [AND condition] THEN INSERT (cols) VALUES (s.cols)&lt;/code&gt;&lt;/strong&gt; — the INSERT branch. Fires when a source row has no target match.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;WHEN NOT MATCHED BY SOURCE [AND condition] THEN DELETE / UPDATE&lt;/code&gt;&lt;/strong&gt; — the DELETE branch. Fires when a target row has no source match. Not universally supported; SQL Server and Snowflake support it, others do not.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 2 — the three-way ANSI split.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;INSERT branch (WHEN NOT MATCHED [BY TARGET]).&lt;/strong&gt; New source rows land in the target. Guaranteed exactly once per source row that has no ON match.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UPDATE branch (WHEN MATCHED).&lt;/strong&gt; Existing target rows are updated in place using values from the joined source row. Can conditionally UPDATE via &lt;code&gt;AND condition&lt;/code&gt; or even DELETE the target row in some dialects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DELETE branch (WHEN NOT MATCHED BY SOURCE).&lt;/strong&gt; Target rows without a source match are removed. Used for full-refresh syncs where source is the authoritative snapshot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The two-branch case (INSERT + UPDATE) is UPSERT.&lt;/strong&gt; No DELETE branch → source can only add or update, never remove. Common for append-heavy pipelines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The three-branch case is full sync.&lt;/strong&gt; Source is the authoritative snapshot; target must mirror it. Common for dimension tables or config tables.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 3 — deterministic evaluation.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Source is scanned once.&lt;/strong&gt; The MERGE reads the source in a single pass. Each source row is joined against the target and routed to one branch. No per-row re-evaluation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target is scanned once (via join).&lt;/strong&gt; For matched rows, the target is read (locked, in some dialects) once during the join.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Each source row triggers exactly one action.&lt;/strong&gt; UPDATE, INSERT, or (with BY SOURCE) DELETE. The branches are mutually exclusive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Row-level triggers fire in ANSI order.&lt;/strong&gt; BEFORE INSERT / BEFORE UPDATE / BEFORE DELETE fire before the row-level action; AFTER equivalents fire after. Some engines have historically buggy trigger fire orders (SQL Server).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Statement-level triggers fire once.&lt;/strong&gt; Total per-branch action counts are reported to the statement-level trigger — one AFTER MERGE trigger sees all inserts / updates / deletes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 4 — the ON condition rules.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best case — equality on a unique key.&lt;/strong&gt; &lt;code&gt;ON t.id = s.id&lt;/code&gt; where &lt;code&gt;t.id&lt;/code&gt; has a primary key or unique index. Deterministic, planner uses index seek, cost is &lt;code&gt;O(source + log(target))&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Composite keys work.&lt;/strong&gt; &lt;code&gt;ON t.tenant_id = s.tenant_id AND t.entity_id = s.entity_id&lt;/code&gt;. Use composite unique index on &lt;code&gt;(tenant_id, entity_id)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-unique ON conditions are dangerous.&lt;/strong&gt; If a source row matches multiple target rows, some engines UPDATE all matched rows; others raise an error; SQL Server has the notorious cardinality bug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Range conditions are allowed but rarely useful.&lt;/strong&gt; &lt;code&gt;ON t.start_ts &amp;lt;= s.event_ts AND t.end_ts &amp;gt; s.event_ts&lt;/code&gt; — a range join. Legal but expensive; often better to precompute the target row via a separate query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filter predicates in ON are pushed to the join.&lt;/strong&gt; Adding &lt;code&gt;AND s.tenant_id = 42&lt;/code&gt; to the ON is equivalent to filtering source before MERGE. Cleaner to filter source in the USING subquery.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 5 — the WHEN MATCHED variants.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;WHEN MATCHED THEN UPDATE SET col = s.col&lt;/code&gt;&lt;/strong&gt; — the classic. Updates all matched rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;WHEN MATCHED AND s.updated_at &amp;gt; t.updated_at THEN UPDATE SET ...&lt;/code&gt;&lt;/strong&gt; — conditional UPDATE. Only updates when the source is fresher. Common for CDC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;WHEN MATCHED AND s.is_deleted THEN DELETE&lt;/code&gt;&lt;/strong&gt; — conditional DELETE via the MATCHED branch. Handy for tombstone events.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;WHEN MATCHED THEN DO NOTHING&lt;/code&gt;&lt;/strong&gt; — some dialects support explicit no-op. Rarely useful; equivalent to not writing the branch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple WHEN MATCHED clauses.&lt;/strong&gt; Some dialects allow multiple; each row picks the first matching condition. Order matters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 6 — the WHEN NOT MATCHED variants.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;WHEN NOT MATCHED [BY TARGET] THEN INSERT (...) VALUES (...)&lt;/code&gt;&lt;/strong&gt; — the classic. Inserts new rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;WHEN NOT MATCHED AND s.op = 'i' THEN INSERT&lt;/code&gt;&lt;/strong&gt; — conditional INSERT. Only insert when the source-op is "insert" (CDC pattern).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;WHEN NOT MATCHED THEN DO NOTHING&lt;/code&gt;&lt;/strong&gt; — explicit no-op. Rare.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;WHEN NOT MATCHED BY SOURCE THEN DELETE&lt;/code&gt;&lt;/strong&gt; — the DELETE branch. Fires for target rows without source matches. Full-sync semantics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;WHEN NOT MATCHED BY SOURCE AND t.status = 'A' THEN UPDATE SET status = 'D'&lt;/code&gt;&lt;/strong&gt; — soft-delete via UPDATE on the BY SOURCE branch. Preserves history.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 7 — the row-level action decision tree.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;For each source row s:&lt;/strong&gt; Find target rows where &lt;code&gt;t.pk = s.pk&lt;/code&gt;. Zero or more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If exactly one target match&lt;/strong&gt; — WHEN MATCHED branch fires. Optional condition filters. Action: UPDATE or DELETE.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If no target match&lt;/strong&gt; — WHEN NOT MATCHED [BY TARGET] branch fires. Action: INSERT.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If multiple target matches&lt;/strong&gt; — depends on dialect. Some UPDATE all (non-deterministic if source values differ per matched row). SQL Server crashes with cardinality error. Some engines allow it only if all matched target rows agree.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;After all source rows processed&lt;/strong&gt; — for each target row NOT matched by any source row, WHEN NOT MATCHED BY SOURCE branch fires (if written).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 8 — the ANSI spec bites.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SQL Server MERGE bugs.&lt;/strong&gt; Long list of known issues — cardinality errors, trigger fire order, plan-cache instability with parameter sniffing. Microsoft documents workarounds. Interview signal: knowing SQL Server MERGE is the buggy one and preferring the CTE-based &lt;code&gt;UPDATE ... FROM ...&lt;/code&gt; for critical paths.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Oracle MERGE.&lt;/strong&gt; ANSI-compliant but the &lt;code&gt;WHEN NOT MATCHED BY SOURCE&lt;/code&gt; branch is missing. Full three-way sync requires a separate DELETE statement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres MERGE.&lt;/strong&gt; Added in Postgres 15. Full ANSI compliance for the three branches. Postgres 17 added &lt;code&gt;WHEN NOT MATCHED BY SOURCE&lt;/code&gt;. Most Postgres code still uses &lt;code&gt;INSERT ... ON CONFLICT&lt;/code&gt; because it predates MERGE.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BigQuery MERGE.&lt;/strong&gt; ANSI-compliant. &lt;code&gt;WHEN NOT MATCHED BY TARGET&lt;/code&gt; is the explicit spelling; the implicit &lt;code&gt;WHEN NOT MATCHED&lt;/code&gt; is also accepted. &lt;code&gt;WHEN NOT MATCHED BY SOURCE&lt;/code&gt; supported.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snowflake MERGE.&lt;/strong&gt; ANSI-compliant with the full three-branch grammar plus optional &lt;code&gt;AND cond&lt;/code&gt; on every branch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Databricks MERGE.&lt;/strong&gt; ANSI-compliant on Delta tables. Photon-accelerated. &lt;code&gt;WHEN MATCHED AND cond THEN DELETE&lt;/code&gt; is idiomatic for tombstones.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;Common beginner mistakes&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Writing MERGE with a non-unique ON condition — cardinality bug or silent data loss.&lt;/li&gt;
&lt;li&gt;Forgetting &lt;code&gt;WHEN NOT MATCHED [BY TARGET]&lt;/code&gt; — some engines assume TARGET, some require the explicit spelling.&lt;/li&gt;
&lt;li&gt;Writing &lt;code&gt;INSERT ... VALUES (t.id, ...)&lt;/code&gt; — you meant &lt;code&gt;s.id&lt;/code&gt;. Common typo.&lt;/li&gt;
&lt;li&gt;Multiple WHEN MATCHED clauses without understanding the order — first matching condition wins per row.&lt;/li&gt;
&lt;li&gt;Using MERGE for high-QPS API upserts — designed for batch. Use &lt;code&gt;ON CONFLICT&lt;/code&gt; on OLTP.&lt;/li&gt;
&lt;li&gt;Assuming DELETE branch (BY SOURCE) exists on all engines — Oracle and MySQL do not have it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — the two-branch MERGE (UPSERT) on Snowflake
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The most common MERGE pattern: dbt-style incremental UPSERT. Source is a batch of new + updated rows; target is the existing table; INSERT new IDs, UPDATE existing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write a Snowflake MERGE that upserts new &lt;code&gt;orders&lt;/code&gt; from the &lt;code&gt;stage_orders&lt;/code&gt; table into &lt;code&gt;analytics.orders&lt;/code&gt;, matching on &lt;code&gt;order_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;stage_orders&lt;/th&gt;
&lt;th&gt;order_id&lt;/th&gt;
&lt;th&gt;customer_id&lt;/th&gt;
&lt;th&gt;total&lt;/th&gt;
&lt;th&gt;updated_at&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;25.00&lt;/td&gt;
&lt;td&gt;2026-07-12 09:00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;td&gt;40.00&lt;/td&gt;
&lt;td&gt;2026-07-12 09:10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;102&lt;/td&gt;
&lt;td&gt;44&lt;/td&gt;
&lt;td&gt;15.00&lt;/td&gt;
&lt;td&gt;2026-07-12 09:15&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Target (analytics.orders) currently has:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;order_id&lt;/th&gt;
&lt;th&gt;customer_id&lt;/th&gt;
&lt;th&gt;total&lt;/th&gt;
&lt;th&gt;updated_at&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;20.00&lt;/td&gt;
&lt;td&gt;2026-07-12 08:00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;MERGE&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;stage_orders&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt;
  &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Source scan — Snowflake reads &lt;code&gt;stage_orders&lt;/code&gt; once (3 rows).&lt;/li&gt;
&lt;li&gt;For each source row, join against target on &lt;code&gt;order_id&lt;/code&gt;. &lt;code&gt;100&lt;/code&gt; matches, &lt;code&gt;101&lt;/code&gt; and &lt;code&gt;102&lt;/code&gt; don't.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;100&lt;/code&gt; — WHEN MATCHED branch fires. UPDATE the target row's &lt;code&gt;customer_id&lt;/code&gt;, &lt;code&gt;total&lt;/code&gt;, &lt;code&gt;updated_at&lt;/code&gt; to source values.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;101&lt;/code&gt; and &lt;code&gt;102&lt;/code&gt; — WHEN NOT MATCHED branch fires. INSERT them with the source values.&lt;/li&gt;
&lt;li&gt;Snowflake reports &lt;code&gt;rows_inserted: 2, rows_updated: 1&lt;/code&gt; in the query result summary.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;order_id&lt;/th&gt;
&lt;th&gt;customer_id&lt;/th&gt;
&lt;th&gt;total&lt;/th&gt;
&lt;th&gt;updated_at&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;25.00&lt;/td&gt;
&lt;td&gt;2026-07-12 09:00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;td&gt;40.00&lt;/td&gt;
&lt;td&gt;2026-07-12 09:10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;102&lt;/td&gt;
&lt;td&gt;44&lt;/td&gt;
&lt;td&gt;15.00&lt;/td&gt;
&lt;td&gt;2026-07-12 09:15&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Two-branch MERGE (INSERT + UPDATE) covers 80% of dbt incremental use cases. Learn this template cold; the rest of the ANSI grammar layers on top.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the three-branch MERGE with DELETE (full sync)
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Full-sync MERGE: source is the authoritative snapshot; target must exactly mirror it. Rows in target that are no longer in source must be deleted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write a Snowflake MERGE that fully syncs the target &lt;code&gt;dim_products&lt;/code&gt; from the source &lt;code&gt;stage_products&lt;/code&gt;. Insert new products, update changed ones, delete products no longer in source.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;stage_products&lt;/th&gt;
&lt;th&gt;product_id&lt;/th&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;10.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;20.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Sprocket&lt;/td&gt;
&lt;td&gt;30.00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Target (dim_products) currently:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;product_id&lt;/th&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;8.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;20.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Doohickey&lt;/td&gt;
&lt;td&gt;15.00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;MERGE&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;dim_products&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;stage_products&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;TARGET&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;SOURCE&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Source scan — 3 rows: &lt;code&gt;(1, Widget, 10)&lt;/code&gt;, &lt;code&gt;(2, Gadget, 20)&lt;/code&gt;, &lt;code&gt;(4, Sprocket, 30)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Target rows: &lt;code&gt;(1, Widget, 8)&lt;/code&gt;, &lt;code&gt;(2, Gadget, 20)&lt;/code&gt;, &lt;code&gt;(3, Doohickey, 15)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;1&lt;/code&gt; — matched. UPDATE branch fires with condition. &lt;code&gt;(t.name &amp;lt;&amp;gt; s.name OR t.price &amp;lt;&amp;gt; s.price)&lt;/code&gt; — name is same, price differs (8 vs 10) → UPDATE. Set price=10.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;2&lt;/code&gt; — matched. Condition — same name and same price → UPDATE skipped (no-op).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;3&lt;/code&gt; — NOT MATCHED BY SOURCE — DELETE branch fires. Row removed.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;4&lt;/code&gt; — NOT MATCHED BY TARGET — INSERT branch fires. Row added.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;product_id&lt;/th&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;10.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;20.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Sprocket&lt;/td&gt;
&lt;td&gt;30.00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Three-branch MERGE (with BY SOURCE DELETE) is the full-sync pattern for dimension tables and config snapshots. Confirm the DELETE branch is intentional — a bad source query can delete production dim rows.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — conditional UPDATE using &lt;code&gt;WHEN MATCHED AND cond&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; CDC apply-loops often want to update only when the source is newer than the target. The &lt;code&gt;AND&lt;/code&gt; clause on WHEN MATCHED expresses this cleanly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write a Snowflake MERGE that upserts CDC events into &lt;code&gt;users&lt;/code&gt;, only updating when the event's &lt;code&gt;updated_at&lt;/code&gt; is newer than the target's &lt;code&gt;updated_at&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- CDC batch (source)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TEMP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;cdc_batch&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'alice-new'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="s1"&gt;'2026-07-12 10:00'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;43&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'bob-STALE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="s1"&gt;'2026-07-12 08:00'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;-- older than target&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Target&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TEMP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'alice-old'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="s1"&gt;'2026-07-12 09:00'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;43&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'bob-fresh'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="s1"&gt;'2026-07-12 09:30'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;MERGE&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;cdc_batch&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Source scan — 2 rows: &lt;code&gt;(42, alice-new, 10:00)&lt;/code&gt; and &lt;code&gt;(43, bob-STALE, 08:00)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Target: &lt;code&gt;(42, alice-old, 09:00)&lt;/code&gt; and &lt;code&gt;(43, bob-fresh, 09:30)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;42&lt;/code&gt; — matched. Condition &lt;code&gt;10:00 &amp;gt; 09:00&lt;/code&gt; → true. UPDATE fires. &lt;code&gt;name → alice-new, updated_at → 10:00&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;43&lt;/code&gt; — matched. Condition &lt;code&gt;08:00 &amp;gt; 09:30&lt;/code&gt; → false. UPDATE skipped — no-op.&lt;/li&gt;
&lt;li&gt;Result — &lt;code&gt;42&lt;/code&gt; updated, &lt;code&gt;43&lt;/code&gt; unchanged. Stale event correctly ignored.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;updated_at&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;alice-new&lt;/td&gt;
&lt;td&gt;2026-07-12 10:00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;td&gt;bob-fresh&lt;/td&gt;
&lt;td&gt;2026-07-12 09:30&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Every CDC apply-loop needs &lt;code&gt;WHEN MATCHED AND s.updated_at &amp;gt; t.updated_at&lt;/code&gt;. Without it, stale events replay corrupt fresh state.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;sql standard merge&lt;/code&gt; interview question on writing a three-branch MERGE
&lt;/h3&gt;

&lt;p&gt;A senior interviewer often asks: "Write a Snowflake MERGE that syncs the &lt;code&gt;stg_customers&lt;/code&gt; staging table into &lt;code&gt;analytics.dim_customers&lt;/code&gt; — insert new customers, update changed customers, and soft-delete customers no longer in source by setting &lt;code&gt;is_active = FALSE&lt;/code&gt; and &lt;code&gt;deactivated_at = CURRENT_TIMESTAMP()&lt;/code&gt;. Do not hard-delete — retain history."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using three-branch MERGE with soft-delete on WHEN NOT MATCHED BY SOURCE
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;MERGE&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dim_customers&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_snapshot_ts&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;stg_customers&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;_batch_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;batch_id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;    &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
 &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;   &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;
 &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;country&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;country&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;country&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;is_active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;deactivated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;TARGET&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deactivated_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;SOURCE&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt;
  &lt;span class="n"&gt;is_active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;deactivated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Rows affected&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Source scan of &lt;code&gt;stg_customers&lt;/code&gt; filtered by batch_id&lt;/td&gt;
&lt;td&gt;~500K rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Join against &lt;code&gt;dim_customers&lt;/code&gt; on &lt;code&gt;id&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;matched + unmatched sets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;WHEN MATCHED AND cols DIFFER → UPDATE (name, email, country, updated_at)&lt;/td&gt;
&lt;td&gt;~50K changed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;WHEN NOT MATCHED BY TARGET → INSERT&lt;/td&gt;
&lt;td&gt;~5K new&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;WHEN NOT MATCHED BY SOURCE AND is_active → UPDATE (soft-delete)&lt;/td&gt;
&lt;td&gt;~1K deactivated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Report row_count summary&lt;/td&gt;
&lt;td&gt;50K+5K+1K = 56K&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rows inserted&lt;/td&gt;
&lt;td&gt;~5,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rows updated (data change)&lt;/td&gt;
&lt;td&gt;~50,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rows soft-deleted&lt;/td&gt;
&lt;td&gt;~1,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rows re-activated (previously inactive, back in source)&lt;/td&gt;
&lt;td&gt;included in UPDATE branch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total wall clock (Snowflake Medium WH, 500K source × 2M target)&lt;/td&gt;
&lt;td&gt;~90 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;&lt;code&gt;IS DISTINCT FROM&lt;/code&gt;&lt;/strong&gt;&lt;/strong&gt; — null-safe inequality. &lt;code&gt;NULL &amp;lt;&amp;gt; NULL&lt;/code&gt; is NULL (falsy in boolean context); &lt;code&gt;NULL IS DISTINCT FROM NULL&lt;/code&gt; is FALSE (correct). Guards against updating rows where a column is null on both sides.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Conditional WHEN MATCHED&lt;/strong&gt;&lt;/strong&gt; — only UPDATE if a column actually changed. Skips writes on rows that arrived unchanged, saving micro-partition rewrite cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Re-activation via WHEN MATCHED UPDATE&lt;/strong&gt;&lt;/strong&gt; — a previously soft-deleted customer that re-appears in source flips &lt;code&gt;is_active = TRUE, deactivated_at = NULL&lt;/code&gt;. Handled inside the MATCHED branch alongside the data update.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Soft-delete via WHEN NOT MATCHED BY SOURCE&lt;/strong&gt;&lt;/strong&gt; — preserves history. Analysts can still query the customer's fields; downstream reports respect &lt;code&gt;is_active&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;&lt;code&gt;AND t.is_active = TRUE&lt;/code&gt; guard on the soft-delete branch&lt;/strong&gt;&lt;/strong&gt; — prevents re-writing already-deactivated rows on subsequent runs. Deactivation is a one-time transition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — MERGE reads source once and target once via the join. On Snowflake, cost is dominated by micro-partition rewrites for the matched+changed set. On Medium WH, 500K source × 2M target with 50K UPDATEs + 5K INSERTs + 1K soft-DELETEs is roughly 90 seconds of wall clock and ~10 credits. Batch-level idempotency: replaying the same batch is a no-op because the IS DISTINCT FROM guard filters unchanged rows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — joins&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL join and merge drills&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/joins" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — SQL&lt;/span&gt;
&lt;strong&gt;SQL practice library&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  3. Postgres ON CONFLICT + MySQL ON DUPLICATE KEY + SQLite UPSERT
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The OLTP-native &lt;code&gt;sql upsert&lt;/code&gt; — &lt;code&gt;postgres on conflict&lt;/code&gt;, &lt;code&gt;mysql on duplicate key update&lt;/code&gt;, and &lt;code&gt;sqlite upsert&lt;/code&gt; — three spellings of the same atomic conflict-resolution primitive
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;on OLTP engines (Postgres, MySQL, SQLite), the idiomatic UPSERT is &lt;em&gt;not&lt;/em&gt; MERGE — it is a special extension of &lt;code&gt;INSERT&lt;/code&gt; that says "if the INSERT would violate a unique constraint, resolve the conflict this way instead of raising an error", and the resolution runs atomically inside the write path with no application-side check and no race window&lt;/strong&gt;. Each engine spells it differently but the semantics are identical: atomic, race-free, single-round-trip.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkrvhhf0fhi297ygift3o.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkrvhhf0fhi297ygift3o.jpeg" alt="Visual diagram of Postgres ON CONFLICT, MySQL ON DUPLICATE KEY UPDATE, and SQLite UPSERT side by side — three dialect cards each showing the INSERT + conflict-resolution syntax, the EXCLUDED / VALUES / excluded pseudo-table reference, and the constraint-targeting rule; on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Slot 1 — Postgres &lt;code&gt;INSERT ... ON CONFLICT ... DO UPDATE / DO NOTHING&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;INSERT INTO t (id, name) VALUES (1, 'a') ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name&lt;/code&gt;&lt;/strong&gt; — the canonical spelling. The &lt;code&gt;(id)&lt;/code&gt; is the &lt;strong&gt;conflict target&lt;/strong&gt; — a column list matching a unique index or primary key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ON CONFLICT ON CONSTRAINT constraint_name DO UPDATE ...&lt;/code&gt;&lt;/strong&gt; — alternate spelling that names the constraint explicitly. Useful when multiple unique indexes exist and you need to specify which one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ON CONFLICT DO NOTHING&lt;/code&gt;&lt;/strong&gt; — the "insert or skip" pattern. Silently drops rows that would conflict. Common for append-only tables where duplicate detection matters more than the actual duplicate values.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;EXCLUDED.col&lt;/code&gt;&lt;/strong&gt; — the pseudo-table representing the row that would have been inserted. Available inside the DO UPDATE clause. &lt;code&gt;SET col = EXCLUDED.col&lt;/code&gt; copies the incoming value; &lt;code&gt;SET col = t.col + EXCLUDED.col&lt;/code&gt; adds them (increment counter pattern).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;WHERE&lt;/code&gt; clause on DO UPDATE.&lt;/strong&gt; &lt;code&gt;ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name WHERE t.updated_at &amp;lt; EXCLUDED.updated_at&lt;/code&gt;. Guards against stale writes overwriting fresh writes. Common for LWW (last-writer-wins) semantics with timestamp guard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;RETURNING&lt;/code&gt;&lt;/strong&gt; — returns the persisted row(s). Distinguishes INSERT vs UPDATE via a &lt;code&gt;RETURNING (xmax = 0) AS was_insert&lt;/code&gt; trick.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;INSERT ... ON CONFLICT ...&lt;/code&gt; is atomic.&lt;/strong&gt; Detects the conflict inside the write path via the unique index. No race window. No application-side check.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 2 — MySQL &lt;code&gt;INSERT ... ON DUPLICATE KEY UPDATE&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;INSERT INTO t (id, name) VALUES (1, 'a') ON DUPLICATE KEY UPDATE name = 'a'&lt;/code&gt;&lt;/strong&gt; — the canonical spelling. Note: no conflict target — any unique-index conflict triggers the DUPLICATE branch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;VALUES(col)&lt;/code&gt; pseudo-function&lt;/strong&gt; — pre-MySQL 8.0.20 spelling. &lt;code&gt;ON DUPLICATE KEY UPDATE name = VALUES(name)&lt;/code&gt; — the &lt;code&gt;VALUES(name)&lt;/code&gt; returns the incoming value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;new.col&lt;/code&gt; row alias (MySQL 8.0.19+).&lt;/strong&gt; &lt;code&gt;INSERT INTO t (id, name) VALUES (1, 'a') AS new ON DUPLICATE KEY UPDATE name = new.name&lt;/code&gt;. Cleaner than &lt;code&gt;VALUES()&lt;/code&gt;. Preferred spelling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No explicit conflict target.&lt;/strong&gt; MySQL treats &lt;em&gt;any&lt;/em&gt; unique-index conflict as a duplicate. If multiple unique indexes exist and different rows conflict on each, behaviour is dialect-specific and often surprising.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ROW_COUNT()&lt;/code&gt; semantics.&lt;/strong&gt; Returns 1 for INSERT, 2 for UPDATE. Handy for detecting which branch fired.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;INSERT IGNORE ...&lt;/code&gt;&lt;/strong&gt; — the MySQL equivalent of &lt;code&gt;ON CONFLICT DO NOTHING&lt;/code&gt;. Silently drops rows that would conflict. Also drops rows that would fail &lt;em&gt;any&lt;/em&gt; insert-time error, which is dangerous — avoid.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;REPLACE INTO — do not use.&lt;/strong&gt; MySQL's &lt;code&gt;REPLACE INTO&lt;/code&gt; is a DELETE-then-INSERT. Breaks FK constraints, fires DELETE triggers, and re-numbers AUTO_INCREMENT. Historical wart; use &lt;code&gt;ON DUPLICATE KEY UPDATE&lt;/code&gt; instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 3 — SQLite &lt;code&gt;INSERT ... ON CONFLICT ... DO UPDATE / DO NOTHING&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;INSERT INTO t (id, name) VALUES (1, 'a') ON CONFLICT (id) DO UPDATE SET name = excluded.name&lt;/code&gt;&lt;/strong&gt; — the canonical spelling. Since SQLite 3.24 (2018). Grammar mirrors Postgres, with the &lt;code&gt;excluded&lt;/code&gt; pseudo-table being lowercase (Postgres uses uppercase &lt;code&gt;EXCLUDED&lt;/code&gt;; both accept either in practice).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conflict target on unique column or unique index.&lt;/strong&gt; &lt;code&gt;ON CONFLICT (id)&lt;/code&gt; — targets the primary key or unique index on &lt;code&gt;id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;WHERE&lt;/code&gt; clause on DO UPDATE.&lt;/strong&gt; Same as Postgres.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;DO NOTHING&lt;/code&gt; supported.&lt;/strong&gt; Same semantics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;INSERT OR REPLACE INTO ...&lt;/code&gt;&lt;/strong&gt; — the legacy SQLite spelling. Also DELETE-then-INSERT, same wart as MySQL's REPLACE.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;INSERT OR IGNORE INTO ...&lt;/code&gt;&lt;/strong&gt; — silently skip on conflict. Same as &lt;code&gt;ON CONFLICT DO NOTHING&lt;/code&gt; but without a target.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 4 — the EXCLUDED / VALUES / excluded pseudo-tables.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Postgres &lt;code&gt;EXCLUDED&lt;/code&gt;&lt;/strong&gt; — represents the row that would have been inserted. Read-only. Reference as &lt;code&gt;EXCLUDED.col&lt;/code&gt;. Available inside &lt;code&gt;DO UPDATE SET ... = EXCLUDED.col&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MySQL &lt;code&gt;VALUES(col)&lt;/code&gt;&lt;/strong&gt; — a function that returns the incoming value for &lt;code&gt;col&lt;/code&gt;. Legacy; MySQL 8.0.20 deprecated it in favor of the &lt;code&gt;new.col&lt;/code&gt; row alias.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MySQL &lt;code&gt;new.col&lt;/code&gt;&lt;/strong&gt; — the row alias set via &lt;code&gt;VALUES (...) AS new&lt;/code&gt;. Preferred in 8.0.19+.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQLite &lt;code&gt;excluded&lt;/code&gt;&lt;/strong&gt; — same as Postgres. Lowercase, but SQLite is case-insensitive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use case — increment counter.&lt;/strong&gt; &lt;code&gt;INSERT INTO counters (id, n) VALUES (1, 1) ON CONFLICT (id) DO UPDATE SET n = counters.n + EXCLUDED.n&lt;/code&gt;. Atomically increments if row exists, initializes to 1 if new.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use case — LWW with timestamp.&lt;/strong&gt; &lt;code&gt;INSERT ... ON CONFLICT (id) DO UPDATE SET val = EXCLUDED.val WHERE EXCLUDED.updated_at &amp;gt; t.updated_at&lt;/code&gt;. Only updates when the incoming row is newer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 5 — constraint targeting.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Postgres — must name a target.&lt;/strong&gt; &lt;code&gt;ON CONFLICT (id) DO UPDATE&lt;/code&gt;. If you omit the target, the syntax is &lt;code&gt;ON CONFLICT DO NOTHING&lt;/code&gt; only (no UPDATE). This is by design — DO UPDATE needs to know which columns' EXCLUDED values to reference.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres — target must match a unique constraint.&lt;/strong&gt; The column list must exactly match a unique or primary key constraint. Partial or expression-index conflicts require &lt;code&gt;ON CONFLICT ON CONSTRAINT&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres partial unique index.&lt;/strong&gt; &lt;code&gt;CREATE UNIQUE INDEX idx ON t (id) WHERE is_active&lt;/code&gt;. Conflict target must include the WHERE clause: &lt;code&gt;ON CONFLICT (id) WHERE is_active DO UPDATE SET ...&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MySQL — no target needed.&lt;/strong&gt; Any unique-index conflict fires the DUPLICATE branch. This is simpler but harder to reason about with multiple unique indexes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQLite — target optional.&lt;/strong&gt; Column list preferred but not required. Similar to MySQL in permissiveness.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 6 — atomicity and locking.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Postgres — row-level lock during conflict resolution.&lt;/strong&gt; The unique-index lookup and subsequent UPDATE happen under an exclusive row lock. Concurrent writers serialize on the same key; different keys go through in parallel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MySQL — gap-locking implications.&lt;/strong&gt; InnoDB uses gap locks and next-key locks during conflict detection. Occasionally causes surprise deadlocks under high concurrency; the default is fine for most workloads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQLite — full DB lock.&lt;/strong&gt; SQLite serialises writes at the database level (single writer). Concurrency is limited but consistency is bulletproof.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All three engines — conflict detection inside the write.&lt;/strong&gt; No two-statement race. The unique-index check happens as part of the INSERT, atomically.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 7 — RETURNING and side effects.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Postgres &lt;code&gt;INSERT ... ON CONFLICT ... RETURNING ...&lt;/code&gt;&lt;/strong&gt; — returns the persisted row (post-INSERT or post-UPDATE). Handy for API layers that need to know the current server-side state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres &lt;code&gt;RETURNING (xmax = 0) AS was_insert&lt;/code&gt;&lt;/strong&gt; — distinguishes INSERT vs UPDATE. &lt;code&gt;xmax = 0&lt;/code&gt; when the row was inserted (no prior version); otherwise the row was updated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MySQL — no RETURNING.&lt;/strong&gt; MariaDB 10.5+ added &lt;code&gt;RETURNING&lt;/code&gt;, but MySQL proper does not. Use &lt;code&gt;SELECT ... FROM ... WHERE id = LAST_INSERT_ID()&lt;/code&gt; or &lt;code&gt;SELECT ROW_COUNT()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQLite &lt;code&gt;INSERT ... ON CONFLICT ... RETURNING ...&lt;/code&gt;&lt;/strong&gt; — supported since 3.35 (2021). Same semantics as Postgres.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trigger fire order.&lt;/strong&gt; All three engines fire triggers appropriately per branch (BEFORE INSERT + AFTER INSERT for the INSERT branch; BEFORE UPDATE + AFTER UPDATE for the UPDATE branch).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 8 — interview probes on OLTP UPSERT.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"When would you use ON CONFLICT DO NOTHING?"&lt;/strong&gt; — Append-only tables where dedup matters more than the values. Common for audit logs, event ledgers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"How do you distinguish INSERT vs UPDATE in Postgres?"&lt;/strong&gt; — &lt;code&gt;RETURNING (xmax = 0) AS was_insert&lt;/code&gt;. &lt;code&gt;xmax = 0&lt;/code&gt; on the returned row means INSERT.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Why is MySQL's REPLACE INTO bad?"&lt;/strong&gt; — DELETE-then-INSERT breaks FK, fires DELETE triggers, and re-numbers AUTO_INCREMENT. Use &lt;code&gt;ON DUPLICATE KEY UPDATE&lt;/code&gt; instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"What's the EXCLUDED pseudo-table?"&lt;/strong&gt; — The row that would have been inserted, accessible in DO UPDATE. &lt;code&gt;EXCLUDED.col&lt;/code&gt; gives you the incoming value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"How do you write a race-safe counter increment?"&lt;/strong&gt; — &lt;code&gt;INSERT INTO counters (id, n) VALUES (1, 1) ON CONFLICT (id) DO UPDATE SET n = counters.n + 1 RETURNING n&lt;/code&gt;. Atomic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"What happens if the INSERT would satisfy multiple unique indexes?"&lt;/strong&gt; — Postgres requires you to name one target. MySQL uses whichever fires first (non-deterministic). SQLite similar to MySQL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;Common beginner mistakes&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Writing &lt;code&gt;ON DUPLICATE KEY UPDATE name = 'a'&lt;/code&gt; — hardcodes the value instead of &lt;code&gt;name = new.name&lt;/code&gt; or &lt;code&gt;name = VALUES(name)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Forgetting the &lt;code&gt;WHERE&lt;/code&gt; clause guard on DO UPDATE — allows stale writes to overwrite fresh writes.&lt;/li&gt;
&lt;li&gt;Using &lt;code&gt;INSERT IGNORE&lt;/code&gt; on MySQL — silently drops errors beyond duplicate keys; masks real issues.&lt;/li&gt;
&lt;li&gt;Using &lt;code&gt;REPLACE INTO&lt;/code&gt; on MySQL — DELETE-then-INSERT wart.&lt;/li&gt;
&lt;li&gt;Not naming the conflict target on Postgres DO UPDATE — Postgres requires it.&lt;/li&gt;
&lt;li&gt;Assuming &lt;code&gt;ON CONFLICT&lt;/code&gt; semantics work on views — they only work on base tables.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — the atomic counter increment on Postgres
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A hit-counter pattern that increments a per-key counter atomically, initializing to 1 on first hit. The atomic &lt;code&gt;ON CONFLICT DO UPDATE SET n = t.n + EXCLUDED.n&lt;/code&gt; is race-free even under thousands of concurrent writers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write a Postgres UPSERT that increments a page-view counter for a given &lt;code&gt;page_id&lt;/code&gt;, initializing to 1 if the row doesn't exist, returning the new count.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;page_views&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page_id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;page_views&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page_views&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;
&lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Client calls with &lt;code&gt;page_id = 42&lt;/code&gt;. First call — no row exists. INSERT branch fires. Row &lt;code&gt;(42, 1)&lt;/code&gt; inserted. RETURNING gives &lt;code&gt;n = 1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Second concurrent call with same &lt;code&gt;page_id = 42&lt;/code&gt;. Row exists. ON CONFLICT branch fires. UPDATE sets &lt;code&gt;n = page_views.n + 1 = 2&lt;/code&gt;. Under exclusive row lock — no race.&lt;/li&gt;
&lt;li&gt;Third concurrent call — same. UPDATE sets &lt;code&gt;n = 3&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Under 10,000 concurrent calls — Postgres serialises writers on the same &lt;code&gt;page_id&lt;/code&gt; via row lock. Different pages go through in parallel.&lt;/li&gt;
&lt;li&gt;No lost increments. Every call increments by exactly 1.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Call&lt;/th&gt;
&lt;th&gt;page_id&lt;/th&gt;
&lt;th&gt;Returned n&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Atomic counter increments are the poster child for OLTP UPSERT. Never use &lt;code&gt;SELECT n THEN UPDATE n = n + 1&lt;/code&gt; — always &lt;code&gt;INSERT ... ON CONFLICT DO UPDATE&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — LWW UPSERT with timestamp guard on Postgres
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A common CDC / cache-invalidation pattern — update the cached row only if the incoming version is newer than the cached version. Guards against out-of-order arrivals.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write a Postgres UPSERT for a session cache where each row has an &lt;code&gt;updated_at&lt;/code&gt; timestamp. Only update if the incoming version is newer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;session_cache&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;session_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;session_cache&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt;
  &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;session_cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Client calls with &lt;code&gt;session_id = X, payload = P, updated_at = 10:00&lt;/code&gt;. Row doesn't exist. INSERT.&lt;/li&gt;
&lt;li&gt;Second call with same session_id, payload = P2, updated_at = 09:00 (older — network delivered out of order). ON CONFLICT branch. WHERE clause: &lt;code&gt;09:00 &amp;gt; 10:00&lt;/code&gt; → false. UPDATE skipped. Row unchanged.&lt;/li&gt;
&lt;li&gt;Third call: payload = P3, updated_at = 11:00. ON CONFLICT branch. WHERE: &lt;code&gt;11:00 &amp;gt; 10:00&lt;/code&gt; → true. UPDATE fires. Row now has P3, 11:00.&lt;/li&gt;
&lt;li&gt;Fourth call: payload = P2, updated_at = 09:00 (very late duplicate). ON CONFLICT. WHERE: &lt;code&gt;09:00 &amp;gt; 11:00&lt;/code&gt; → false. Skipped.&lt;/li&gt;
&lt;li&gt;Idempotent — the same duplicate calls have no effect after the first application.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Call&lt;/th&gt;
&lt;th&gt;payload arg&lt;/th&gt;
&lt;th&gt;updated_at arg&lt;/th&gt;
&lt;th&gt;Row state after&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;P&lt;/td&gt;
&lt;td&gt;10:00&lt;/td&gt;
&lt;td&gt;(P, 10:00)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;P2&lt;/td&gt;
&lt;td&gt;09:00 (stale)&lt;/td&gt;
&lt;td&gt;(P, 10:00) unchanged&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;P3&lt;/td&gt;
&lt;td&gt;11:00&lt;/td&gt;
&lt;td&gt;(P3, 11:00)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;P2&lt;/td&gt;
&lt;td&gt;09:00 (dup)&lt;/td&gt;
&lt;td&gt;(P3, 11:00) unchanged&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; LWW UPSERT with timestamp guard is the standard cache-pattern. Never write ordered assumptions into your API client — the pattern handles late/duplicate arrivals correctly at the write.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — MySQL UPSERT with the row alias
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; MySQL 8.0.19+ ships a cleaner UPSERT syntax with row-aliasing. Prefer this over &lt;code&gt;VALUES()&lt;/code&gt; in new code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write a MySQL 8 UPSERT for the &lt;code&gt;page_views&lt;/code&gt; example, using the row alias.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;page_views&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page_id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;page_views&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;DUPLICATE&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page_views&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;AS new&lt;/code&gt; — alias the incoming row. Subsequent references to &lt;code&gt;new.n&lt;/code&gt; return the incoming value.&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;page_id&lt;/code&gt; doesn't exist — INSERT branch. Row &lt;code&gt;(42, 1)&lt;/code&gt; inserted.&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;page_id&lt;/code&gt; exists — ON DUPLICATE KEY branch. UPDATE &lt;code&gt;n = page_views.n + new.n&lt;/code&gt;. Row goes from &lt;code&gt;(42, N)&lt;/code&gt; to &lt;code&gt;(42, N+1)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Pre-MySQL-8.0.19 spelling: &lt;code&gt;... ON DUPLICATE KEY UPDATE n = page_views.n + VALUES(n)&lt;/code&gt;. Both work; new is cleaner.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SELECT ROW_COUNT()&lt;/code&gt; — returns 1 for insert, 2 for update. Distinguish INSERT vs UPDATE this way.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Call&lt;/th&gt;
&lt;th&gt;Row state after&lt;/th&gt;
&lt;th&gt;ROW_COUNT()&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;First&lt;/td&gt;
&lt;td&gt;(42, 1)&lt;/td&gt;
&lt;td&gt;1 (INSERT)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Second&lt;/td&gt;
&lt;td&gt;(42, 2)&lt;/td&gt;
&lt;td&gt;2 (UPDATE)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Third&lt;/td&gt;
&lt;td&gt;(42, 3)&lt;/td&gt;
&lt;td&gt;2 (UPDATE)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; MySQL 8+ ships two UPSERT spellings — &lt;code&gt;VALUES()&lt;/code&gt; (legacy) and &lt;code&gt;AS new&lt;/code&gt; (row alias). Prefer the row alias.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;postgres on conflict&lt;/code&gt; interview question on multi-column conflict target
&lt;/h3&gt;

&lt;p&gt;A senior interviewer asks: "You have a &lt;code&gt;orders_by_customer_month&lt;/code&gt; table with a composite PK &lt;code&gt;(customer_id, year_month)&lt;/code&gt; tracking monthly order totals. Write a Postgres UPSERT that atomically adds a new order's total to the monthly counter, initializing on first order per customer per month."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using composite conflict target + atomic increment via EXCLUDED
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders_by_customer_month&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;year_month&lt;/span&gt; &lt;span class="nb"&gt;CHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;-- e.g. '2026-07'&lt;/span&gt;
  &lt;span class="n"&gt;total_amount&lt;/span&gt; &lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;order_count&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;last_updated&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;year_month&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- The upsert&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;orders_by_customer_month&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;year_month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_updated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TO_CHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'YYYY-MM'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;year_month&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt;
  &lt;span class="n"&gt;total_amount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orders_by_customer_month&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_amount&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;order_count&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orders_by_customer_month&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_count&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;last_updated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;total_amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;(cust=42, month=2026-07, amount=25)&lt;/th&gt;
&lt;th&gt;State after&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;First order for cust 42 in July 2026&lt;/td&gt;
&lt;td&gt;INSERT → (42, '2026-07', 25.00, 1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Second order for cust 42 in July, amount 40&lt;/td&gt;
&lt;td&gt;CONFLICT → UPDATE → total=65.00, count=2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;First order for cust 43 in July, amount 30&lt;/td&gt;
&lt;td&gt;INSERT → (43, '2026-07', 30.00, 1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Third order for cust 42 in July, amount 15&lt;/td&gt;
&lt;td&gt;CONFLICT → UPDATE → total=80.00, count=3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;First order for cust 42 in August, amount 20&lt;/td&gt;
&lt;td&gt;INSERT → (42, '2026-08', 20.00, 1)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;customer_id&lt;/th&gt;
&lt;th&gt;year_month&lt;/th&gt;
&lt;th&gt;total_amount&lt;/th&gt;
&lt;th&gt;order_count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;2026-07&lt;/td&gt;
&lt;td&gt;80.00&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;2026-08&lt;/td&gt;
&lt;td&gt;20.00&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;td&gt;2026-07&lt;/td&gt;
&lt;td&gt;30.00&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Composite conflict target&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;ON CONFLICT (customer_id, year_month)&lt;/code&gt; matches the composite PK. Row-level lock is scoped to the specific &lt;code&gt;(customer, month)&lt;/code&gt; bucket, so different customers or different months proceed in parallel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;&lt;code&gt;EXCLUDED.total_amount&lt;/code&gt; + running-total via reference to the target&lt;/strong&gt;&lt;/strong&gt; — the DO UPDATE clause reads &lt;code&gt;orders_by_customer_month.total_amount&lt;/code&gt; (current stored value) and adds &lt;code&gt;EXCLUDED.total_amount&lt;/code&gt; (incoming order's amount). Atomic under exclusive row lock; no read-modify-write race.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;&lt;code&gt;+ 1&lt;/code&gt; for order_count&lt;/strong&gt;&lt;/strong&gt; — increment by exactly 1 per order. Race-free because the entire UPSERT runs under the row lock.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;&lt;code&gt;NOW()&lt;/code&gt; in both branches&lt;/strong&gt;&lt;/strong&gt; — timestamp always updated to the moment of the write, whether it was an INSERT or an UPDATE. Handy for observability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;&lt;code&gt;RETURNING&lt;/code&gt;&lt;/strong&gt;&lt;/strong&gt; — the API can return &lt;code&gt;{total_amount, order_count}&lt;/code&gt; after the write in the same round-trip. No follow-up SELECT needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — per-call &lt;code&gt;O(log n)&lt;/code&gt; on the composite unique index. Under 10,000 concurrent writers, throughput is bounded by lock contention on hot &lt;code&gt;(customer, month)&lt;/code&gt; buckets; for cold buckets, throughput scales linearly with connections.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — SQL&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL practice library&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — indexing&lt;/span&gt;
&lt;strong&gt;Composite unique index drills&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/indexing" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  4. Snowflake + BigQuery + Databricks MERGE
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Warehouse-native &lt;code&gt;snowflake merge&lt;/code&gt;, &lt;code&gt;bigquery merge&lt;/code&gt;, and Databricks MERGE — the same ANSI grammar, three different storage-rewrite models, three different cost surfaces
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;on modern warehouses, MERGE is the canonical bulk-idempotent-write primitive; Snowflake rewrites the affected micro-partitions, BigQuery rewrites the affected partitions, Databricks/Delta rewrites the affected Parquet files, and the cost surface for a given MERGE is roughly (rows_affected / rows_per_partition) × (partition_rewrite_cost) plus the source-scan cost&lt;/strong&gt;. Understanding the storage-rewrite model per engine is the difference between a 30-second dbt incremental and a 30-minute one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh51s852olwda87ih7tw9.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh51s852olwda87ih7tw9.jpeg" alt="Visual diagram of warehouse MERGE dialects across Snowflake, BigQuery, and Databricks — three side-by-side cards showing the MERGE INTO ... USING ... syntax, the per-engine storage rewrite model (micro-partitions vs partitions vs Parquet file rewrites), and the cost model (warehouse credits vs slot_ms + bytes billed vs DBUs); on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Slot 1 — Snowflake MERGE anatomy.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Grammar&lt;/strong&gt; — full ANSI three-branch: &lt;code&gt;MERGE INTO t USING s ON t.pk = s.pk WHEN MATCHED [AND cond] THEN UPDATE / DELETE WHEN NOT MATCHED [AND cond] THEN INSERT&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage rewrite&lt;/strong&gt; — Snowflake rewrites the micro-partitions containing matched target rows. If your MERGE touches rows in 47 of 8,000 partitions, only those 47 are rewritten. Immutable, versioned; old micro-partitions retained per Time Travel retention.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost&lt;/strong&gt; — warehouse tier × wall clock. A Medium warehouse for 2 minutes ≈ 4 credits (~$8 at $2/credit). Optimising MERGE means minimising rewritten partitions (via CLUSTER BY on the ON column) and minimising wall clock (via appropriate WH sizing).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Query Profile&lt;/strong&gt; — MERGE shows as a plan with source scan, target scan (via join), and a &lt;code&gt;Delete/Insert&lt;/code&gt; operator at the top. Read partition-scan and pruning % on both source and target.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cluster keys&lt;/strong&gt; — cluster the target on the merge key (&lt;code&gt;t.pk&lt;/code&gt;) so target scan prunes. Cluster the source on the merge key for source-side pruning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrent MERGE handling&lt;/strong&gt; — Snowflake serialises concurrent MERGEs on the same target table by default. Concurrent MERGEs against different partitions may parallelise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;STREAM ... ON TABLE&lt;/code&gt;&lt;/strong&gt; — combines with MERGE for CDC-style incremental loads. The stream captures inserts/updates/deletes on the source; MERGE consumes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;WHEN MATCHED AND is_deleted THEN DELETE&lt;/code&gt;&lt;/strong&gt; — supports tombstone-driven deletes cleanly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;WHEN NOT MATCHED BY SOURCE THEN DELETE&lt;/code&gt;&lt;/strong&gt; — full-sync deletes supported.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Return value&lt;/strong&gt; — Snowflake reports &lt;code&gt;number of rows inserted / updated / deleted&lt;/code&gt; in the query result summary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 2 — BigQuery MERGE anatomy.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Grammar&lt;/strong&gt; — full ANSI three-branch: &lt;code&gt;MERGE INTO t USING s ON t.pk = s.pk WHEN MATCHED [AND cond] THEN UPDATE / DELETE WHEN NOT MATCHED BY TARGET [AND cond] THEN INSERT WHEN NOT MATCHED BY SOURCE [AND cond] THEN UPDATE / DELETE&lt;/code&gt;. The &lt;code&gt;BY TARGET&lt;/code&gt; / &lt;code&gt;BY SOURCE&lt;/code&gt; spellings are explicit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage rewrite&lt;/strong&gt; — BigQuery rewrites the affected partitions. Each partition is a set of columnar files; MERGE rewrites the entire partition, not individual rows. This is why partition alignment on the MERGE key is critical.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost — on-demand.&lt;/strong&gt; &lt;code&gt;$5/TB scanned&lt;/code&gt; + rewrite cost. Full-table MERGE on a 100 TB unpartitioned table scans 100 TB → $500 per MERGE.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost — flat-rate.&lt;/strong&gt; &lt;code&gt;slot_ms&lt;/code&gt;. MERGE consumes slots for source-scan + join + rewrite. A well-planned MERGE completes in tens of thousands of slot_ms; a badly-planned one in millions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--dry_run&lt;/code&gt;&lt;/strong&gt; — estimate bytes-processed before running. Free. Use in CI to gate expensive MERGEs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partition alignment&lt;/strong&gt; — if the source's date range covers only July 2026, BigQuery can prune to partitions &lt;code&gt;20260701..20260731&lt;/code&gt; on the target if the ON clause includes a partition column filter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cluster keys&lt;/strong&gt; — cluster the target on the MERGE key for block-level pruning inside partitions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrent MERGE handling&lt;/strong&gt; — BigQuery serialises writes to a table. Concurrent MERGEs queue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;WHEN NOT MATCHED BY SOURCE&lt;/code&gt;&lt;/strong&gt; — supported. Requires a full target scan; expensive on large tables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Return value&lt;/strong&gt; — the job's &lt;code&gt;numDmlAffectedRows&lt;/code&gt; metadata field reports total rows affected.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 3 — Databricks / Delta Lake MERGE anatomy.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Grammar&lt;/strong&gt; — full ANSI three-branch on Delta tables. Non-Delta tables (Parquet, CSV, etc.) do NOT support MERGE.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage rewrite&lt;/strong&gt; — Delta MERGE rewrites the affected Parquet files (typically 100 MB – 1 GB per file). Reads the affected files, applies the branches, writes new files, updates the Delta transaction log to point at the new files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost — DBUs.&lt;/strong&gt; Databricks Units are the compute-time unit. MERGE cost = DBU/hr × wall clock. Photon acceleration reduces wall clock and DBU cost by 3-5× typical.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File pruning&lt;/strong&gt; — Delta uses data-skipping via min/max per file for the MERGE key. Cluster / Z-order on the MERGE key for pruning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrent MERGE handling&lt;/strong&gt; — Delta uses optimistic concurrency control. Two concurrent MERGEs to the same table may both start; one commits, the other detects the conflict on commit and retries (with the latest snapshot).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;WHEN MATCHED AND cond THEN UPDATE / DELETE&lt;/code&gt;&lt;/strong&gt; — full conditional branches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;WHEN NOT MATCHED AND cond THEN INSERT&lt;/code&gt;&lt;/strong&gt; — full support.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;WHEN NOT MATCHED BY SOURCE&lt;/code&gt;&lt;/strong&gt; — supported since Databricks Runtime 12.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;CHANGE DATA FEED&lt;/code&gt;&lt;/strong&gt; — Delta's built-in CDC feed. Combine with downstream MERGE for incremental pipelines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;OPTIMIZE ... ZORDER BY&lt;/code&gt;&lt;/strong&gt; — periodic maintenance to reduce fragmentation from many small MERGE writes. Rewrite small files into big ones, z-order by the MERGE key.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 4 — the storage-rewrite math per engine.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Snowflake — per micro-partition.&lt;/strong&gt; Micro-partition size is ~50-500 MB uncompressed. A MERGE touching 47 micro-partitions rewrites 47 × ~200 MB = ~9 GB of storage. Cost is proportional.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BigQuery — per partition.&lt;/strong&gt; Partition size varies (daily partitions may be MB or TB). A MERGE touching 30 partitions rewrites all 30 in full — even if only 1 row per partition matched.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delta — per Parquet file.&lt;/strong&gt; File size default ~1 GB; can be tuned. A MERGE touching 100 files rewrites those 100 files = ~100 GB of storage. &lt;code&gt;OPTIMIZE&lt;/code&gt; periodically compacts to keep files at target size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimising rewrite cost.&lt;/strong&gt; Cluster / partition / Z-order the target on the MERGE key so the affected files are localised, not spread across all files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The "small MERGE, big cost" trap.&lt;/strong&gt; A MERGE that updates 100 rows spread across 100 partitions rewrites 100 partitions. Same MERGE on a table clustered by the merge key rewrites 1 partition. Same cost model on all three engines.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 5 — cost surface per engine.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Snowflake.&lt;/strong&gt; Warehouse tier × wall clock. Optimise for shorter wall clock at given tier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BigQuery on-demand.&lt;/strong&gt; Bytes scanned × $5/TB + rewrite. Optimise for smaller bytes scanned via partition prune.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BigQuery flat-rate.&lt;/strong&gt; Slot commitment × month. Optimise for slot efficiency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Databricks.&lt;/strong&gt; DBU/hr × wall clock. Photon acceleration is often the biggest lever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cluster / partition / z-order investments.&lt;/strong&gt; Rewrite the target's physical layout once; benefit every subsequent MERGE.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 6 — CDC apply-loops with &lt;code&gt;STREAM&lt;/code&gt; / CHANGES.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Snowflake &lt;code&gt;CREATE STREAM ON TABLE source_events&lt;/code&gt;&lt;/strong&gt; — captures inserts/updates/deletes. Combined with MERGE, gives you an idempotent CDC apply-loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snowflake &lt;code&gt;TASK&lt;/code&gt;&lt;/strong&gt; — schedule the MERGE from the stream, runs every N minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BigQuery &lt;code&gt;INFORMATION_SCHEMA.STREAMING_TABLES&lt;/code&gt;&lt;/strong&gt; — not a native CDC feed; use Datastream or third-party.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delta &lt;code&gt;CHANGES&lt;/code&gt; feed&lt;/strong&gt; — enable &lt;code&gt;spark.databricks.delta.properties.defaults.enableChangeDataFeed = true&lt;/code&gt;. Downstream reads the CDC feed and applies via MERGE.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Common pattern&lt;/strong&gt; — source stream → dedup by key → MERGE with timestamp guard → target dimension table.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 7 — the 7 most common warehouse MERGE pathologies.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unpartitioned target on BigQuery.&lt;/strong&gt; MERGE scans the full table on every run. Fix — &lt;code&gt;PARTITION BY DATE(created_at)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unclustered target on Snowflake.&lt;/strong&gt; MERGE rewrites micro-partitions across the whole table. Fix — &lt;code&gt;CLUSTER BY (merge_key)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delta table without OPTIMIZE.&lt;/strong&gt; Many small files from frequent MERGEs. Fix — schedule &lt;code&gt;OPTIMIZE t ZORDER BY (merge_key)&lt;/code&gt; weekly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SELECT *&lt;/code&gt; in source.&lt;/strong&gt; Reads more columns than needed. Fix — project only the merge key + updated columns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-unique ON condition.&lt;/strong&gt; Ambiguous WHEN MATCHED, silent data loss. Fix — dedup source before MERGE.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MERGE on the wrong join order.&lt;/strong&gt; Fix — the smaller side goes on the right. Warehouses generally figure this out, but hints help.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No timestamp guard.&lt;/strong&gt; Stale events overwrite fresh. Fix — &lt;code&gt;WHEN MATCHED AND s.updated_at &amp;gt; t.updated_at&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;Common beginner mistakes&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Using MERGE on unpartitioned BigQuery tables — costs 100× more than needed.&lt;/li&gt;
&lt;li&gt;Forgetting &lt;code&gt;WHEN NOT MATCHED BY TARGET&lt;/code&gt; on BigQuery — the explicit spelling is required in some contexts.&lt;/li&gt;
&lt;li&gt;Running Delta MERGE on non-Delta tables — silent failure (Parquet tables reject MERGE).&lt;/li&gt;
&lt;li&gt;Not scheduling &lt;code&gt;OPTIMIZE ZORDER BY&lt;/code&gt; on Delta — files fragment into thousands of tiny ones.&lt;/li&gt;
&lt;li&gt;Assuming Snowflake serialises all MERGE writers — different partitions can parallelise.&lt;/li&gt;
&lt;li&gt;Non-deterministic source rows — duplicate keys in source produce undefined behaviour on WHEN MATCHED across all three engines.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — dbt incremental on Snowflake, from raw batch to MERGE
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The daily dbt incremental pattern on Snowflake. Loads yesterday's orders into a snapshot table, then MERGEs into the analytics dim table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the full Snowflake MERGE for the dbt incremental with &lt;code&gt;unique_key='order_id'&lt;/code&gt;, timestamp guard on &lt;code&gt;updated_at&lt;/code&gt;, and a &lt;code&gt;_dbt_updated_at&lt;/code&gt; tracker column on the target.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt; stg_orders has ~1M rows (yesterday's batch), dim_orders has ~500M rows (all-time).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;MERGE&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dim_orders&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;_dbt_updated_at&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
      &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;staging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stg_orders&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;_batch_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;batch_id&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt;
  &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;_dbt_updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_dbt_updated_at&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_dbt_updated_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;VALUES&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_dbt_updated_at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Source subquery dedupes the batch by &lt;code&gt;order_id&lt;/code&gt;, keeping the latest &lt;code&gt;updated_at&lt;/code&gt; per order.&lt;/li&gt;
&lt;li&gt;Target scan on &lt;code&gt;dim_orders&lt;/code&gt; uses the cluster key on &lt;code&gt;order_id&lt;/code&gt; for pruning. Only micro-partitions containing matching order_ids are touched.&lt;/li&gt;
&lt;li&gt;WHEN MATCHED AND timestamp — updates only when source is fresher. Prevents stale replays from overwriting.&lt;/li&gt;
&lt;li&gt;WHEN NOT MATCHED — inserts new order_ids into the target.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;_dbt_updated_at&lt;/code&gt; — a target-side observability column tracking when dbt last touched the row. Common in dbt patterns.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Source rows scanned&lt;/td&gt;
&lt;td&gt;1,000,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source rows deduped&lt;/td&gt;
&lt;td&gt;950,000 (some order_ids had multiple events)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Target rows scanned (matched)&lt;/td&gt;
&lt;td&gt;800,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rows updated&lt;/td&gt;
&lt;td&gt;750,000 (50K stale, guard skipped)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rows inserted&lt;/td&gt;
&lt;td&gt;150,000 (new orders)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wall clock on Medium WH&lt;/td&gt;
&lt;td&gt;45 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Credits used&lt;/td&gt;
&lt;td&gt;~1.5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; dbt incremental on Snowflake is the canonical warehouse MERGE. Cluster on &lt;code&gt;unique_key&lt;/code&gt;, dedup source in the USING subquery, use timestamp guard on WHEN MATCHED.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — BigQuery MERGE with partition prune predicate
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; BigQuery MERGE cost is dominated by target-scan bytes. Adding a partition-prune predicate to the ON or WHERE clause is the single biggest optimisation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write a BigQuery MERGE that upserts July 2026 orders from &lt;code&gt;stage_orders&lt;/code&gt; into &lt;code&gt;dim_orders&lt;/code&gt; (partitioned by &lt;code&gt;DATE(created_at)&lt;/code&gt;, clustered by &lt;code&gt;order_id&lt;/code&gt;), with a partition-prune predicate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Target&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="nv"&gt;`project.analytics.dim_orders`&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;CLUSTER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Source&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="nv"&gt;`project.staging.stage_orders`&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;_batch_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'BATCH-2026-07-12'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;MERGE&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="nv"&gt;`project.analytics.dim_orders`&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt;
    &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;`project.staging.stage_orders`&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;
   &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2026-07-01'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2026-07-31'&lt;/span&gt;
   &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2026-07-01'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2026-07-31'&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt;
  &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;TARGET&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;DATE(t.created_at) BETWEEN ... AND ...&lt;/code&gt; in the ON — the partition-prune predicate. BigQuery uses this to skip all partitions outside July 2026.&lt;/li&gt;
&lt;li&gt;Source subquery — &lt;code&gt;SELECT DISTINCT&lt;/code&gt; to dedup. In production, prefer &lt;code&gt;ROW_NUMBER()&lt;/code&gt; with a timestamp guard.&lt;/li&gt;
&lt;li&gt;WHEN MATCHED — timestamp guard.&lt;/li&gt;
&lt;li&gt;WHEN NOT MATCHED BY TARGET — INSERT the new order.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--dry_run&lt;/code&gt; — verify bytes-processed. Should be roughly &lt;code&gt;31 partitions × avg_partition_size + source_size&lt;/code&gt;. Full-table scan without the prune would be &lt;code&gt;all_partitions × avg_partition_size&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query&lt;/th&gt;
&lt;th&gt;Bytes processed&lt;/th&gt;
&lt;th&gt;Cost (on-demand @ $5/TB)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Without partition prune&lt;/td&gt;
&lt;td&gt;12 TB&lt;/td&gt;
&lt;td&gt;$60 per MERGE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;With partition prune&lt;/td&gt;
&lt;td&gt;400 GB&lt;/td&gt;
&lt;td&gt;$2 per MERGE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speedup&lt;/td&gt;
&lt;td&gt;30×&lt;/td&gt;
&lt;td&gt;30× cost saving&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Every BigQuery MERGE against a partitioned target needs a partition-prune predicate. Verify with &lt;code&gt;--dry_run&lt;/code&gt; before running.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — Databricks Delta MERGE with tombstone deletes
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; CDC events often carry a soft-delete flag or &lt;code&gt;op='d'&lt;/code&gt; marker. The Delta MERGE handles inserts / updates / deletes in one statement using conditional branches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write a Databricks MERGE that consumes a Kafka batch of CDC events (with &lt;code&gt;op&lt;/code&gt; in &lt;code&gt;{'i', 'u', 'd'}&lt;/code&gt;) and applies to a Delta target table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt; &lt;code&gt;cdc_batch&lt;/code&gt; table with columns &lt;code&gt;id, name, op, updated_at&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;MERGE&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users_delta&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
      &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;cdc_batch&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'d'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'i'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'u'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'i'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'u'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Source dedup — keep the latest per id (by updated_at).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WHEN MATCHED AND s.op = 'd' THEN DELETE&lt;/code&gt; — tombstones become target deletes.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WHEN MATCHED AND s.op IN ('i', 'u') AND fresher THEN UPDATE&lt;/code&gt; — updates with timestamp guard.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WHEN NOT MATCHED AND s.op IN ('i', 'u') THEN INSERT&lt;/code&gt; — inserts new rows.&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;op = 'd'&lt;/code&gt; and the row doesn't exist in target — no-op (WHEN NOT MATCHED with &lt;code&gt;op = 'd'&lt;/code&gt; is not written; the branch is silently skipped). Correct behaviour: delete of a nonexistent row is a no-op.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CDC event&lt;/th&gt;
&lt;th&gt;Target action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;(42, 'alice', 'i', ...)&lt;/code&gt; new&lt;/td&gt;
&lt;td&gt;INSERT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;(42, 'alice-2', 'u', ...)&lt;/code&gt; existing, fresher&lt;/td&gt;
&lt;td&gt;UPDATE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;(42, '', 'd', ...)&lt;/code&gt; existing&lt;/td&gt;
&lt;td&gt;DELETE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;(99, '', 'd', ...)&lt;/code&gt; not in target&lt;/td&gt;
&lt;td&gt;no-op&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Multi-branch conditional MERGE is the modern CDC apply pattern. Learn it once; ship it everywhere.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;bigquery merge&lt;/code&gt; interview question on cost-optimised MERGE
&lt;/h3&gt;

&lt;p&gt;A senior interviewer asks: "You're on-call for a daily dbt incremental that MERGEs 5M orders into a 2B-row BigQuery table, and it's costing $500/day. Walk me through the diagnosis and the top three optimisations."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using dry-run + partition prune + cluster on merge key
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Step 1 — Confirm cost via --dry_run&lt;/span&gt;
&lt;span class="n"&gt;bq&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="c1"&gt;--dry_run --use_legacy_sql=false '&lt;/span&gt;
&lt;span class="n"&gt;MERGE&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="nv"&gt;`project.analytics.dim_orders`&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;staging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stg_orders&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;_batch_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;"current"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;TARGET&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="s1"&gt;';
-- Output: This query will process 100 TB when run.

-- Step 2 — Repartition + cluster the target (one-time cost)
CREATE OR REPLACE TABLE `project.analytics.dim_orders`
PARTITION BY DATE(created_at)
CLUSTER BY order_id
AS SELECT * FROM `project.analytics.dim_orders`;

-- Step 3 — Add partition prune predicate to future MERGEs
MERGE INTO `project.analytics.dim_orders` AS t
USING (
  SELECT DISTINCT * FROM staging.stg_orders WHERE _batch_id = "current"
) AS s
ON t.order_id = s.order_id
  AND DATE(t.created_at) &amp;gt;= CURRENT_DATE() - 7
  AND DATE(s.created_at) &amp;gt;= CURRENT_DATE() - 7
WHEN MATCHED AND s.updated_at &amp;gt; t.updated_at THEN UPDATE SET ...
WHEN NOT MATCHED BY TARGET THEN INSERT ...;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;th&gt;Bytes processed&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Ratio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Baseline (unpartitioned)&lt;/td&gt;
&lt;td&gt;100 TB&lt;/td&gt;
&lt;td&gt;$500&lt;/td&gt;
&lt;td&gt;1×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Partition target&lt;/td&gt;
&lt;td&gt;3 TB&lt;/td&gt;
&lt;td&gt;$15&lt;/td&gt;
&lt;td&gt;33× cheaper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ Partition prune predicate&lt;/td&gt;
&lt;td&gt;300 GB&lt;/td&gt;
&lt;td&gt;$1.50&lt;/td&gt;
&lt;td&gt;333× cheaper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ Cluster on order_id&lt;/td&gt;
&lt;td&gt;100 GB&lt;/td&gt;
&lt;td&gt;$0.50&lt;/td&gt;
&lt;td&gt;1000× cheaper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ Materialised view for hot filter&lt;/td&gt;
&lt;td&gt;~30 GB&lt;/td&gt;
&lt;td&gt;$0.15&lt;/td&gt;
&lt;td&gt;3333× cheaper&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;th&gt;Effect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repartition target&lt;/td&gt;
&lt;td&gt;Daily partitions align with source date range&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cluster on order_id&lt;/td&gt;
&lt;td&gt;Blocks pruned inside partitions on the merge key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Partition prune predicate&lt;/td&gt;
&lt;td&gt;Explicit filter in ON scoping affected partitions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source dedup&lt;/td&gt;
&lt;td&gt;Prevents multi-match cardinality issues&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timestamp guard&lt;/td&gt;
&lt;td&gt;Idempotent replay&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Partition target on DATE(created_at)&lt;/strong&gt;&lt;/strong&gt; — daily partitions. Source batches typically cover a small date window; the MERGE prunes to just those partitions. 300× cost reduction on typical dbt loads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cluster on order_id&lt;/strong&gt;&lt;/strong&gt; — the MERGE key. BigQuery uses block-level metadata to skip blocks that can't contain matching order_ids. 3-10× on top of partition prune.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Partition prune predicate in ON&lt;/strong&gt;&lt;/strong&gt; — explicit hint to the planner: "we only care about the last 7 days of data". Prevents accidental full-table scans when source has stale data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;&lt;code&gt;--dry_run&lt;/code&gt; before every deploy&lt;/strong&gt;&lt;/strong&gt; — catches accidental full-table MERGEs in CI. Fail the PR if estimated bytes &amp;gt; 10 TB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Materialised view (final step)&lt;/strong&gt;&lt;/strong&gt; — for dashboards that repeatedly query the MERGE output, an MV avoids re-scanning the target on read. Combined with BI Engine, dashboards are sub-second.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — one-time re-partition + cluster cost is ~$500 for a 2 TB target rewrite. Ongoing per-MERGE cost drops from $500/day to &amp;lt;$1/day. Payback in one day.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — optimization&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL optimization drills&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/optimization" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — SQL&lt;/span&gt;
&lt;strong&gt;SQL practice library&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  5. Anti-patterns and dialect matrix
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The &lt;code&gt;sql merge upsert&lt;/code&gt; failure modes — SQL Server's cardinality bug, concurrent MERGE races, non-deterministic sources — and the 8-engine dialect matrix that ties it all together
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;MERGE and UPSERT are the correct primitives for idempotent writes, but they ship with a well-known set of failure modes — non-deterministic source rows, concurrent-writer lost updates, the SQL Server MERGE cardinality bug, and trigger fire-order weirdness — that every senior engineer knows to defend against with dedup, unique indexes, and (when necessary) explicit serialization&lt;/strong&gt;. Master the failure modes and you can write MERGEs that survive concurrency, retries, and data-drift.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvaxj8jza3sywkl03rpqe.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvaxj8jza3sywkl03rpqe.jpeg" alt="Visual diagram of MERGE anti-patterns and the 8-engine dialect matrix — left a 4-row anti-pattern card listing SQL Server MERGE cardinality bug, trigger fire order, concurrent lost-update race, and non-deterministic source rows, right an 8-engine matrix showing MERGE / UPSERT / ON CONFLICT / ON DUPLICATE KEY syntax per engine; on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Slot 1 — the SQL Server MERGE cardinality bug.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The bug.&lt;/strong&gt; When a MERGE has multiple source rows matching the same target row, SQL Server &lt;em&gt;sometimes&lt;/em&gt; silently produces incorrect results — either running the UPDATE branch multiple times per target row (non-deterministic final value) or attempting both an UPDATE and an INSERT for the same target row (constraint violation).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reproduction.&lt;/strong&gt; Simple MERGE with a non-unique ON condition or a source with duplicate keys. Search for "SQL Server MERGE bug" — Microsoft's own docs acknowledge these issues.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documented issues (Microsoft KB).&lt;/strong&gt; Multiple &lt;code&gt;Cannot insert duplicate key&lt;/code&gt; errors, race conditions with concurrent inserts, trigger fire-order surprises.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workaround 1 — dedup source before MERGE.&lt;/strong&gt; Use a CTE with &lt;code&gt;ROW_NUMBER()&lt;/code&gt; to guarantee unique source keys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workaround 2 — use INSERT + UPDATE separately.&lt;/strong&gt; Ship an INSERT ... WHERE NOT EXISTS + UPDATE ... FROM in a transaction. Verbose but bug-free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workaround 3 — use SERIALIZABLE isolation.&lt;/strong&gt; Prevents the race but hurts throughput.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Community recommendation.&lt;/strong&gt; Many senior SQL Server DBAs recommend avoiding MERGE for critical paths and using explicit INSERT / UPDATE via CTE instead. Aaron Bertrand's "Please stop using MERGE" is the canonical write-up.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 2 — trigger fire order.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;BEFORE INSERT vs BEFORE UPDATE per branch.&lt;/strong&gt; MERGE fires the appropriate BEFORE trigger for each row's branch (INSERT branch → BEFORE INSERT; UPDATE branch → BEFORE UPDATE). Simple in principle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AFTER triggers fire per statement, not per branch.&lt;/strong&gt; After the MERGE completes, statement-level AFTER triggers fire once, seeing the union of INSERTs / UPDATEs / DELETEs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL Server historical bugs.&lt;/strong&gt; In some SQL Server versions, AFTER INSERT and AFTER UPDATE triggers on the same table both fire twice due to MERGE's compilation. Patched in later versions but worth verifying in your environment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres — clean.&lt;/strong&gt; Row-level BEFORE triggers fire per row per branch. Statement-level AFTER triggers fire once for the entire MERGE.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interview signal.&lt;/strong&gt; Knowing that trigger behaviour on MERGE differs subtly across engines is a senior-level detail.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 3 — concurrent MERGE races.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The setup.&lt;/strong&gt; Two workers both start a MERGE on the same target key. Under READ COMMITTED, both see the pre-write target state. Both attempt to INSERT the same key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What happens on Postgres.&lt;/strong&gt; The unique index catches the second INSERT and raises a unique-violation error. If the second MERGE was inside an atomic UPSERT (&lt;code&gt;INSERT ... ON CONFLICT&lt;/code&gt;), the error is caught internally and routed to UPDATE.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What happens on Snowflake.&lt;/strong&gt; Snowflake serialises MERGE writes on the same table via a Cluster-Table Lock. The second MERGE blocks until the first commits, then runs on the fresh state. No race.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What happens on BigQuery.&lt;/strong&gt; BigQuery queues concurrent DML. Second MERGE runs after first commits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What happens on Delta.&lt;/strong&gt; Optimistic concurrency — both start, first commits, second detects the conflict at commit and retries with the fresh snapshot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The safe pattern.&lt;/strong&gt; Use atomic UPSERT on OLTP (INSERT ... ON CONFLICT). Use MERGE with unique ON condition on warehouses. Rely on engine-level serialisation for the concurrent-write safety.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 4 — non-deterministic source rows.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The problem.&lt;/strong&gt; Source contains multiple rows with the same MERGE key. WHEN MATCHED fires once per source row per matched target row — the target's final state depends on the order in which source rows are processed. Order is not deterministic on parallel plans.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The signal.&lt;/strong&gt; UPDATE runs multiple times on the same target row; final target value depends on which source row was processed last. Different runs of the same MERGE produce different final states.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix — dedup source before MERGE.&lt;/strong&gt; &lt;code&gt;ROW_NUMBER() OVER (PARTITION BY id ORDER BY updated_at DESC)&lt;/code&gt; in the USING subquery.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix — aggregate source before MERGE.&lt;/strong&gt; &lt;code&gt;SELECT id, MAX(updated_at) AS updated_at, ARRAY_AGG(name) FROM source GROUP BY id&lt;/code&gt;. Preserves determinism.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL Server specific.&lt;/strong&gt; With multiple source matches, SQL Server may attempt both UPDATE and INSERT on the same target — the notorious cardinality error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snowflake / BigQuery.&lt;/strong&gt; Both accept multiple matches but final target state is undefined. Effectively equivalent to dedup being your responsibility.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 5 — the safe MERGE checklist.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unique source keys.&lt;/strong&gt; Dedup source before MERGE using &lt;code&gt;ROW_NUMBER()&lt;/code&gt; or &lt;code&gt;GROUP BY&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unique target keys.&lt;/strong&gt; Ensure the ON condition uses a primary key or unique index. Composite is fine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timestamp guard.&lt;/strong&gt; &lt;code&gt;WHEN MATCHED AND s.updated_at &amp;gt; t.updated_at&lt;/code&gt; for CDC / event workloads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;IS DISTINCT FROM&lt;/code&gt; for null-safe compare.&lt;/strong&gt; When conditional-updating on column difference.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotency test.&lt;/strong&gt; Run the MERGE twice on the same batch — output should be identical after both runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cardinality assertion.&lt;/strong&gt; For SQL Server, wrap MERGE in a check that raises if source cardinality doesn't match unique keys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost dry-run.&lt;/strong&gt; For BigQuery, run &lt;code&gt;--dry_run&lt;/code&gt; first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cluster target on ON key.&lt;/strong&gt; For warehouse MERGE, cluster the target for pruning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollback plan.&lt;/strong&gt; For Time-Travel-supported engines (Snowflake, Delta), you can rollback the target if a MERGE ships bad data. Know the retention.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 6 — the 8-engine dialect matrix (one-line spellings).&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;th&gt;Native UPSERT syntax&lt;/th&gt;
&lt;th&gt;MERGE support&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Postgres 15+&lt;/td&gt;
&lt;td&gt;&lt;code&gt;INSERT ... ON CONFLICT (col) DO UPDATE SET col = EXCLUDED.col&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Full ANSI MERGE (PG 15+); BY SOURCE in PG 17+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL 8&lt;/td&gt;
&lt;td&gt;&lt;code&gt;INSERT ... AS new ON DUPLICATE KEY UPDATE col = new.col&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No native MERGE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQLite 3.24+&lt;/td&gt;
&lt;td&gt;&lt;code&gt;INSERT ... ON CONFLICT (col) DO UPDATE SET col = excluded.col&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No native MERGE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL Server 2008+&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MERGE INTO ... USING ... WHEN MATCHED THEN UPDATE / WHEN NOT MATCHED THEN INSERT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Full ANSI MERGE (with known bugs); prefer INSERT + UPDATE via CTE for critical paths&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Oracle 9i+&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MERGE INTO ... USING ... WHEN MATCHED THEN UPDATE ... WHEN NOT MATCHED THEN INSERT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Full ANSI MERGE (no BY SOURCE)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snowflake&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MERGE INTO ... USING ... WHEN MATCHED [AND cond] THEN UPDATE / WHEN NOT MATCHED THEN INSERT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Full ANSI MERGE with BY SOURCE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BigQuery&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MERGE INTO ... USING ... WHEN MATCHED THEN UPDATE / WHEN NOT MATCHED BY TARGET THEN INSERT / WHEN NOT MATCHED BY SOURCE THEN DELETE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Full ANSI MERGE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Databricks (Delta)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MERGE INTO delta_table USING ... WHEN MATCHED [AND cond] THEN UPDATE / DELETE / WHEN NOT MATCHED THEN INSERT / WHEN NOT MATCHED BY SOURCE THEN DELETE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Full ANSI MERGE on Delta tables only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Slot 7 — the 5-step senior MERGE reading strategy.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Step 1 — inspect the ON condition.&lt;/strong&gt; Is it unique on both sides? If not, dedup source.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 2 — inspect WHEN MATCHED guard.&lt;/strong&gt; Is there a timestamp / condition guard? If not, replays are non-idempotent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 3 — inspect source dedup.&lt;/strong&gt; Does the USING subquery have &lt;code&gt;ROW_NUMBER()&lt;/code&gt; or &lt;code&gt;GROUP BY&lt;/code&gt;? If not, multiple-match risk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 4 — inspect target index / cluster.&lt;/strong&gt; Is the target clustered on the ON key? If not, MERGE reads more than needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 5 — inspect concurrency.&lt;/strong&gt; Is this the only writer? If not, understand engine-level serialisation guarantees.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 8 — when NOT to use MERGE.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;High-QPS single-row API calls.&lt;/strong&gt; Use OLTP UPSERT (&lt;code&gt;INSERT ... ON CONFLICT&lt;/code&gt;). MERGE is batch-oriented; per-call cost is higher.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL Server critical paths.&lt;/strong&gt; Use INSERT + UPDATE via CTE. Avoid the known bugs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Append-only tables.&lt;/strong&gt; Use &lt;code&gt;INSERT ... ON CONFLICT DO NOTHING&lt;/code&gt;. MERGE overhead is unnecessary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full-refresh dimension loads.&lt;/strong&gt; TRUNCATE + INSERT can be simpler than three-branch MERGE with WHEN NOT MATCHED BY SOURCE.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-database MERGE.&lt;/strong&gt; Not universally supported. Materialise the source into a local staging table first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;Common beginner mistakes&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Using MERGE on SQL Server for critical paths — subject to known bugs.&lt;/li&gt;
&lt;li&gt;Non-unique ON condition — non-deterministic UPDATE.&lt;/li&gt;
&lt;li&gt;Assuming concurrent MERGEs are magically safe — engine-level serialisation varies.&lt;/li&gt;
&lt;li&gt;Forgetting that MERGE is batch — using it for API single-row upserts.&lt;/li&gt;
&lt;li&gt;Not deduping source — the #1 cause of MERGE bugs across all engines.&lt;/li&gt;
&lt;li&gt;Ignoring trigger fire order — MERGE fires per-branch, not per-statement.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — SQL Server MERGE cardinality bug and the CTE workaround
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Reproduce the SQL Server MERGE cardinality bug, then show the safe CTE-based rewrite.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Show a SQL Server MERGE that triggers the cardinality bug and the safe rewrite using INSERT + UPDATE via CTE.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt; Source has two rows with the same PK; target has one matching row.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code — buggy MERGE.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Buggy: source has two rows with same PK&lt;/span&gt;
&lt;span class="n"&gt;MERGE&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'alice'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'alice-dup'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- Msg 8672: The MERGE statement attempted to UPDATE or DELETE the same row more than once.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Code — safe rewrite.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Safe: dedup source, then explicit INSERT + UPDATE via CTE&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;deduped&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'alice'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'alice-dup'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="k"&gt;source&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;deduped&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="k"&gt;source&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;source&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Buggy MERGE — source has two rows for id=42. Target has one. WHEN MATCHED fires twice per source row against the same target row. SQL Server raises 8672 error.&lt;/li&gt;
&lt;li&gt;Safe rewrite — first dedup source via ROW_NUMBER() in a CTE. Then run explicit UPDATE FROM CTE and INSERT WHERE NOT EXISTS in a transaction.&lt;/li&gt;
&lt;li&gt;Same conceptual output; no MERGE, no bug.&lt;/li&gt;
&lt;li&gt;Aaron Bertrand's blog "Please stop using MERGE" documents this pattern extensively.&lt;/li&gt;
&lt;li&gt;On other engines (Snowflake, BigQuery, Databricks), the same deduped MERGE works fine — the bug is specific to SQL Server.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Buggy MERGE (dup source)&lt;/td&gt;
&lt;td&gt;Msg 8672 error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deduped MERGE&lt;/td&gt;
&lt;td&gt;Works fine on non-SQL-Server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CTE + separate UPDATE + INSERT&lt;/td&gt;
&lt;td&gt;Works on all engines including SQL Server&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; On SQL Server, always dedup source before MERGE, and consider CTE + explicit INSERT + UPDATE for critical paths.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — race-safe concurrent UPSERT under 10K QPS
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A high-QPS API upsert on Postgres. Multiple concurrent workers all hit the same key; the correct pattern is atomic &lt;code&gt;INSERT ... ON CONFLICT&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Show the race-safe concurrent UPSERT pattern for a &lt;code&gt;page_views&lt;/code&gt; counter with 10 K QPS on a single key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;page_views&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page_id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;page_views&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page_views&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Worker A calls with &lt;code&gt;page_id = 42&lt;/code&gt;. Row doesn't exist. INSERT. Row &lt;code&gt;(42, 1)&lt;/code&gt;. Return 1.&lt;/li&gt;
&lt;li&gt;Worker B calls concurrently with &lt;code&gt;page_id = 42&lt;/code&gt;. Row now exists (or is being inserted). Under exclusive row lock, worker B waits for A's commit, then sees &lt;code&gt;(42, 1)&lt;/code&gt;. UPDATE fires: &lt;code&gt;n = 1 + 1 = 2&lt;/code&gt;. Return 2.&lt;/li&gt;
&lt;li&gt;Under 10K QPS on same key — Postgres serialises writers on the &lt;code&gt;page_id = 42&lt;/code&gt; row. Throughput is bounded by row-lock hold time (~microseconds). Cluster-wide throughput ~50K QPS on modern hardware for hot keys; scales linearly for distributed keys.&lt;/li&gt;
&lt;li&gt;No lost increments. Every call increments by exactly 1.&lt;/li&gt;
&lt;li&gt;Comparison — MERGE for this pattern would be much slower (higher per-call overhead), and it's harder to guarantee atomicity across concurrent MERGEs. Native &lt;code&gt;ON CONFLICT&lt;/code&gt; is the right tool.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concurrent workers&lt;/th&gt;
&lt;th&gt;Final n after 10,000 calls&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For high-QPS atomic increments, always use native &lt;code&gt;INSERT ... ON CONFLICT DO UPDATE&lt;/code&gt; on Postgres/SQLite (or &lt;code&gt;ON DUPLICATE KEY UPDATE&lt;/code&gt; on MySQL). Never MERGE — the per-call overhead is too high for API calls.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the SCD Type 2 pattern with MERGE
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; SCD Type 2 preserves history — every change to a dimension row creates a new version, with &lt;code&gt;valid_from&lt;/code&gt; / &lt;code&gt;valid_to&lt;/code&gt; timestamps. MERGE handles it in two passes: close the current version, insert the new version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the SCD Type 2 MERGE on Snowflake for a &lt;code&gt;dim_customer&lt;/code&gt; table with history preservation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dim_customer&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;valid_from&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;valid_to&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'9999-12-31'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;is_current&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_current&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;dim_customer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;is_current&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Step 1: Close current versions of rows that will get a new version&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dim_customer&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;valid_to&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;is_current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;FALSE&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;is_current&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stg_customer&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 2: Insert new versions (including brand-new customers)&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dim_customer&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valid_from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valid_to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s1"&gt;'9999-12-31'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stg_customer&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dim_customer&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_current&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
   &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Step 1 closes existing "current" rows that have a changed source row. Sets &lt;code&gt;valid_to = NOW()&lt;/code&gt; and &lt;code&gt;is_current = FALSE&lt;/code&gt;. History preserved.&lt;/li&gt;
&lt;li&gt;Step 2 inserts new rows for (a) brand-new customers not in target, and (b) existing customers whose columns changed (they now have a closed old row and need a fresh current row).&lt;/li&gt;
&lt;li&gt;Unique index on &lt;code&gt;(customer_id) WHERE is_current&lt;/code&gt; ensures at most one current row per customer at any time.&lt;/li&gt;
&lt;li&gt;Some engines allow this in a single MERGE with &lt;code&gt;WHEN MATCHED THEN UPDATE ... AND then INSERT&lt;/code&gt;. Snowflake accepts multi-branch MERGE for this pattern.&lt;/li&gt;
&lt;li&gt;dbt's &lt;code&gt;snapshot&lt;/code&gt; macro implements this exact pattern with additional book-keeping.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;customer_id&lt;/th&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;email&lt;/th&gt;
&lt;th&gt;valid_from&lt;/th&gt;
&lt;th&gt;valid_to&lt;/th&gt;
&lt;th&gt;is_current&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;alice-v1&lt;/td&gt;
&lt;td&gt;&lt;a href="mailto:a@old.com"&gt;a@old.com&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;2026-01-01&lt;/td&gt;
&lt;td&gt;2026-07-12 09:00&lt;/td&gt;
&lt;td&gt;FALSE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;alice-v2&lt;/td&gt;
&lt;td&gt;&lt;a href="mailto:a@new.com"&gt;a@new.com&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;2026-07-12 09:00&lt;/td&gt;
&lt;td&gt;9999-12-31&lt;/td&gt;
&lt;td&gt;TRUE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;td&gt;bob&lt;/td&gt;
&lt;td&gt;&lt;a href="mailto:b@x.com"&gt;b@x.com&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;2026-07-12 09:00&lt;/td&gt;
&lt;td&gt;9999-12-31&lt;/td&gt;
&lt;td&gt;TRUE&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; SCD Type 2 needs history preservation — never overwrite the old row. Use two-step MERGE (close-then-insert) or dbt's snapshot macro.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;mysql on duplicate key update&lt;/code&gt; interview question on choosing among UPSERT, MERGE, and REPLACE
&lt;/h3&gt;

&lt;p&gt;A senior interviewer asks: "You're consulting on a MySQL application that ships REPLACE INTO for its user-profile save endpoint at 5K QPS. Why is this a bad choice, and what would you change?"&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using ON DUPLICATE KEY UPDATE with row alias + RETURNING via ROW_COUNT()
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- BEFORE (bad): REPLACE INTO&lt;/span&gt;
&lt;span class="c1"&gt;-- Semantics: DELETE if exists, then INSERT — breaks FK, fires DELETE triggers,&lt;/span&gt;
&lt;span class="c1"&gt;-- re-numbers AUTO_INCREMENT.&lt;/span&gt;
&lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- AFTER (good): INSERT ... ON DUPLICATE KEY UPDATE with row alias&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;DUPLICATE&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Bonus: distinguish INSERT vs UPDATE via ROW_COUNT()&lt;/span&gt;
&lt;span class="c1"&gt;-- 1 → insert, 2 → update, 0 → no-op (row unchanged if you added a WHERE-like guard)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ROW_COUNT&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;REPLACE (bad)&lt;/th&gt;
&lt;th&gt;ON DUPLICATE KEY UPDATE (good)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Client sends PUT /users/42&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;DELETE FROM users WHERE id=42 → FK cascades kick in&lt;/td&gt;
&lt;td&gt;INSERT attempts → PK conflict on id=42&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;INSERT users (42, ...) → AUTO_INCREMENT bumps&lt;/td&gt;
&lt;td&gt;ON DUPLICATE branch → UPDATE cols&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Delete triggers fire (audit-log spam)&lt;/td&gt;
&lt;td&gt;Update triggers fire (correct)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;If FK child rows referenced user 42 → violation OR cascade&lt;/td&gt;
&lt;td&gt;No FK impact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Latency: 2 SQL statements internally&lt;/td&gt;
&lt;td&gt;Latency: 1 statement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Idempotent? YES but destructive to FK graph&lt;/td&gt;
&lt;td&gt;Idempotent AND safe&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;REPLACE&lt;/th&gt;
&lt;th&gt;ON DUPLICATE KEY UPDATE&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FK integrity&lt;/td&gt;
&lt;td&gt;Breaks (cascade may delete child rows)&lt;/td&gt;
&lt;td&gt;Preserved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DELETE triggers fired&lt;/td&gt;
&lt;td&gt;YES (audit spam)&lt;/td&gt;
&lt;td&gt;NO&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UPDATE triggers fired&lt;/td&gt;
&lt;td&gt;NO (only DELETE + INSERT)&lt;/td&gt;
&lt;td&gt;YES (correct semantic)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AUTO_INCREMENT bumps&lt;/td&gt;
&lt;td&gt;YES (gap in id sequence)&lt;/td&gt;
&lt;td&gt;NO&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wall clock per call&lt;/td&gt;
&lt;td&gt;~2 ms&lt;/td&gt;
&lt;td&gt;~1 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Correctness under FK&lt;/td&gt;
&lt;td&gt;Breaks&lt;/td&gt;
&lt;td&gt;Preserved&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;&lt;code&gt;INSERT ... ON DUPLICATE KEY UPDATE&lt;/code&gt; preserves FK graph&lt;/strong&gt;&lt;/strong&gt; — the target row's PK doesn't change; foreign key references from child tables remain valid. REPLACE would DELETE the row (potentially cascading), then INSERT a fresh row with the same id but different internal identity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Correct trigger semantics&lt;/strong&gt;&lt;/strong&gt; — UPDATE triggers fire on the UPDATE branch; INSERT triggers fire on the INSERT branch. REPLACE fires DELETE triggers on every "update", which is wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;No AUTO_INCREMENT bump&lt;/strong&gt;&lt;/strong&gt; — the target row keeps its id; no gap in the sequence. REPLACE bumps AUTO_INCREMENT because internally it's a fresh INSERT.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Row alias &lt;code&gt;AS new&lt;/code&gt;&lt;/strong&gt;&lt;/strong&gt; — MySQL 8.0.19+ cleaner spelling. Preferred over the legacy &lt;code&gt;VALUES()&lt;/code&gt; function.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;&lt;code&gt;ROW_COUNT()&lt;/code&gt; for distinguishing INSERT vs UPDATE&lt;/strong&gt;&lt;/strong&gt; — returns 1 for insert, 2 for update. Useful in application code that needs to know which branch fired.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — one statement per call, ~1 ms wall clock. At 5K QPS, total DB CPU is bounded and predictable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — SQL&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL practice library&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — joins/sql&lt;/span&gt;
&lt;strong&gt;SQL join drills&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/joins/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  Cheat sheet — MERGE / UPSERT recipe list
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two primitives, one contract.&lt;/strong&gt; MERGE (ANSI, three-branch) for batch idempotent writes; native UPSERT (&lt;code&gt;INSERT ... ON CONFLICT&lt;/code&gt; / &lt;code&gt;ON DUPLICATE KEY UPDATE&lt;/code&gt;) for OLTP single-row atomic upserts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never SELECT-then-INSERT.&lt;/strong&gt; Two-statement pattern has a race window. Always single-statement atomic UPSERT.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never REPLACE INTO on MySQL.&lt;/strong&gt; DELETE-then-INSERT wart. Use &lt;code&gt;ON DUPLICATE KEY UPDATE&lt;/code&gt; instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres UPSERT skeleton.&lt;/strong&gt; &lt;code&gt;INSERT INTO t (id, v) VALUES (?, ?) ON CONFLICT (id) DO UPDATE SET v = EXCLUDED.v&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres UPSERT with guard.&lt;/strong&gt; &lt;code&gt;... WHERE t.updated_at &amp;lt; EXCLUDED.updated_at&lt;/code&gt; — LWW semantics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres counter increment.&lt;/strong&gt; &lt;code&gt;INSERT INTO c (id, n) VALUES (?, 1) ON CONFLICT (id) DO UPDATE SET n = c.n + 1&lt;/code&gt; — atomic increment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres INSERT-or-skip.&lt;/strong&gt; &lt;code&gt;INSERT ... ON CONFLICT (id) DO NOTHING&lt;/code&gt; — no update on conflict.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres RETURNING.&lt;/strong&gt; Add &lt;code&gt;RETURNING (xmax = 0) AS was_insert, *&lt;/code&gt; to distinguish INSERT vs UPDATE and return the persisted row.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MySQL UPSERT skeleton (8.0.19+).&lt;/strong&gt; &lt;code&gt;INSERT INTO t (id, v) VALUES (?, ?) AS new ON DUPLICATE KEY UPDATE v = new.v&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MySQL UPSERT skeleton (pre-8.0.19).&lt;/strong&gt; &lt;code&gt;INSERT INTO t (id, v) VALUES (?, ?) ON DUPLICATE KEY UPDATE v = VALUES(v)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MySQL branch detection.&lt;/strong&gt; &lt;code&gt;ROW_COUNT()&lt;/code&gt; returns 1 for INSERT, 2 for UPDATE.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQLite UPSERT skeleton.&lt;/strong&gt; &lt;code&gt;INSERT INTO t (id, v) VALUES (?, ?) ON CONFLICT (id) DO UPDATE SET v = excluded.v&lt;/code&gt; — same as Postgres.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ANSI MERGE skeleton.&lt;/strong&gt; &lt;code&gt;MERGE INTO t USING s ON t.pk = s.pk WHEN MATCHED THEN UPDATE SET ... WHEN NOT MATCHED THEN INSERT ...&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ANSI MERGE three-branch (full sync).&lt;/strong&gt; Add &lt;code&gt;WHEN NOT MATCHED BY SOURCE THEN DELETE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timestamp guard.&lt;/strong&gt; &lt;code&gt;WHEN MATCHED AND s.updated_at &amp;gt; t.updated_at THEN UPDATE&lt;/code&gt; — protects against stale-event overwrites.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch dedup.&lt;/strong&gt; Wrap source in &lt;code&gt;ROW_NUMBER() OVER (PARTITION BY key ORDER BY updated_at DESC) = 1&lt;/code&gt; — one row per key. Every warehouse MERGE needs this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL Server MERGE bugs.&lt;/strong&gt; Cardinality error on multiple source matches, plan-cache surprises. Prefer CTE + INSERT + UPDATE for critical paths.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL Server safe rewrite.&lt;/strong&gt; &lt;code&gt;WITH deduped AS (...) UPDATE FROM deduped; INSERT FROM deduped WHERE NOT EXISTS&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Oracle MERGE.&lt;/strong&gt; Full ANSI two-branch. No &lt;code&gt;WHEN NOT MATCHED BY SOURCE&lt;/code&gt; (must separate).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snowflake MERGE.&lt;/strong&gt; Full ANSI three-branch. Cluster target on merge key for pruning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snowflake MERGE cost.&lt;/strong&gt; Warehouse tier × wall clock. Rewrites affected micro-partitions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BigQuery MERGE.&lt;/strong&gt; Full ANSI three-branch. Partition + cluster target. Use &lt;code&gt;--dry_run&lt;/code&gt; first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BigQuery MERGE cost.&lt;/strong&gt; On-demand: bytes billed × $5/TB. Flat-rate: slot_ms. Partition prune predicate in ON is essential.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delta MERGE.&lt;/strong&gt; Full ANSI three-branch on Delta tables (Parquet won't accept). Photon accelerates. Schedule OPTIMIZE ZORDER BY periodically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimistic concurrency (Delta).&lt;/strong&gt; Two concurrent MERGEs — first commits, second retries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SCD Type 2 pattern.&lt;/strong&gt; Two-step: close current rows, insert new versions. Preserves history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CDC apply-loop pattern.&lt;/strong&gt; Dedup source, WHEN MATCHED AND fresher, WHEN NOT MATCHED INSERT, WHEN MATCHED AND is_deleted DELETE.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API upsert pattern.&lt;/strong&gt; &lt;code&gt;PUT /users/:id&lt;/code&gt; handler → single &lt;code&gt;INSERT ... ON CONFLICT DO UPDATE&lt;/code&gt; — no race window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bulk load pattern.&lt;/strong&gt; COPY into staging → MERGE from staging → drop staging.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When OFF the beaten path.&lt;/strong&gt; For very high QPS or cross-database, consider queue-based writes (Kafka → downstream apply job) instead of direct UPSERT / MERGE.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Return values across engines.&lt;/strong&gt; Postgres/SQLite: &lt;code&gt;RETURNING&lt;/code&gt;. MySQL: &lt;code&gt;SELECT LAST_INSERT_ID() / ROW_COUNT()&lt;/code&gt;. Warehouses: query result summary (rows inserted / updated / deleted).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotency test.&lt;/strong&gt; Run MERGE twice on same batch. Same final state = correct. Different state = bug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollback plan.&lt;/strong&gt; Snowflake Time Travel, Delta version history. Retain enough history to rollback a bad MERGE.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;
&lt;h3&gt;
  
  
  What's the difference between MERGE and UPSERT?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;MERGE&lt;/strong&gt; is the ANSI SQL:2003 statement with up to three branches — &lt;code&gt;WHEN MATCHED THEN UPDATE / DELETE&lt;/code&gt;, &lt;code&gt;WHEN NOT MATCHED [BY TARGET] THEN INSERT&lt;/code&gt;, &lt;code&gt;WHEN NOT MATCHED BY SOURCE THEN UPDATE / DELETE&lt;/code&gt; — designed for batch idempotent writes. It scans the source once and joins against the target via the ON condition, routing each row to exactly one branch. &lt;strong&gt;UPSERT&lt;/strong&gt; is the informal term for "insert if new, update if existing"; on Postgres and SQLite it's spelled &lt;code&gt;INSERT ... ON CONFLICT (col) DO UPDATE SET col = EXCLUDED.col&lt;/code&gt;, on MySQL it's &lt;code&gt;INSERT ... ON DUPLICATE KEY UPDATE col = new.col&lt;/code&gt;, and on warehouses it's a two-branch MERGE. UPSERT is the OLTP-native primitive optimised for single-row, high-QPS atomic writes with conflict detection inside the write path (via unique index). MERGE is the warehouse-native primitive optimised for batch idempotent loads that can affect thousands or millions of rows in a single statement. Both provide the same idempotency contract; MERGE is the superset (adds full-sync DELETE), UPSERT is the two-branch case (INSERT + UPDATE only). Rule of thumb — high-QPS API endpoints use UPSERT; batch pipelines (dbt incremental, CDC apply-loops) use MERGE.&lt;/p&gt;
&lt;h3&gt;
  
  
  When should I use INSERT ... ON CONFLICT vs MERGE in Postgres?
&lt;/h3&gt;

&lt;p&gt;Postgres 15+ ships both, so the question is real. &lt;strong&gt;Use &lt;code&gt;INSERT ... ON CONFLICT (col) DO UPDATE SET&lt;/code&gt;&lt;/strong&gt; for single-row API upserts, high-QPS backend writes, atomic counter increments, and any workload that fits the "insert one row, resolve via unique index" pattern. It's atomic, low-latency (~1 ms), race-free, and the community-blessed idiom. &lt;strong&gt;Use &lt;code&gt;MERGE INTO&lt;/code&gt;&lt;/strong&gt; for batch loads — dbt incrementals, CDC apply-loops, bulk staging-to-analytics moves, and full-sync patterns that need &lt;code&gt;WHEN NOT MATCHED BY SOURCE&lt;/code&gt; (delete-if-not-in-source semantics). MERGE is more expressive (three branches with conditions), but has higher per-call overhead — you don't want it for single-row API calls at 10K QPS. In practice, most Postgres codebases stick with &lt;code&gt;INSERT ... ON CONFLICT&lt;/code&gt; for everything except full-sync scenarios, because it was the only idiomatic option pre-Postgres 15 and it's already the shared reflex across the Postgres community. New Postgres 15+ codebases can start using MERGE for batch, but &lt;code&gt;ON CONFLICT&lt;/code&gt; remains the correct choice for OLTP.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why does SQL Server's MERGE have known bugs?
&lt;/h3&gt;

&lt;p&gt;SQL Server's MERGE has a documented history of correctness issues — cardinality errors when multiple source rows match the same target row (raising &lt;code&gt;Msg 8672: The MERGE statement attempted to UPDATE or DELETE the same row more than once&lt;/code&gt;), trigger fire-order surprises (double-firing on both INSERT and UPDATE triggers in some patched-and-repatched versions), plan-cache instability with parameter sniffing, race conditions with concurrent inserts even under SERIALIZABLE isolation, and semantic surprises with &lt;code&gt;WHEN NOT MATCHED BY SOURCE&lt;/code&gt; on partitioned tables. Aaron Bertrand's widely-referenced "Please stop using MERGE" post (updated across the years) catalogs the specific issues and Microsoft's Connect / Feedback item history acknowledging them. The safe pattern on SQL Server for critical paths is to dedup source in a CTE and then run explicit &lt;code&gt;UPDATE FROM CTE&lt;/code&gt; and &lt;code&gt;INSERT ... WHERE NOT EXISTS&lt;/code&gt; inside a transaction — verbose, but bug-free and equivalent in semantics. On other engines (Postgres, Snowflake, BigQuery, Databricks), MERGE works as specified and is the correct choice. If you're on SQL Server and you must use MERGE, always: (1) dedup the source before merging, (2) test with concurrency, (3) verify trigger fire-order in your environment, (4) prefer INSERT + UPDATE via CTE for anything mission-critical.&lt;/p&gt;
&lt;h3&gt;
  
  
  How do I write an idempotent MERGE in Snowflake vs BigQuery?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Snowflake&lt;/strong&gt; — the canonical dbt-incremental pattern. Source-scan-once, cluster target on the merge key, dedup source in the USING subquery, timestamp guard on WHEN MATCHED, and let Snowflake serialise concurrent writers automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;MERGE&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dim_orders&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders_batch&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;BigQuery&lt;/strong&gt; — same shape but add a &lt;strong&gt;partition-prune predicate&lt;/strong&gt; in the ON clause so the target scan skips partitions outside the source's date range. Without this, BigQuery scans the full target and costs 30–100× more:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;MERGE&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="nv"&gt;`project.analytics.dim_orders`&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders_batch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
   &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2026-07-01'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2026-07-31'&lt;/span&gt;
   &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2026-07-01'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2026-07-31'&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;TARGET&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="p"&gt;...;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify BigQuery costs with &lt;code&gt;bq query --dry_run&lt;/code&gt; before running — if estimated bytes exceed expectations, add or refine the partition prune predicate. In both engines, idempotency is guaranteed by the timestamp guard + source dedup — replaying the same batch is a no-op after the first run.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the race condition in a naive UPSERT?
&lt;/h3&gt;

&lt;p&gt;The classic race: two workers concurrently receive the same request (&lt;code&gt;PUT /users/42&lt;/code&gt;). Both open a transaction. Both run &lt;code&gt;SELECT id FROM users WHERE id = 42&lt;/code&gt; and get "no row." Both decide to INSERT. Under READ COMMITTED isolation, both attempt the INSERT — one wins (commits first), the other hits a UNIQUE-constraint violation on the primary key and raises an error to the client. Symptoms — sporadic 500 errors under load, audit-log noise (&lt;code&gt;Duplicate key violation&lt;/code&gt; in every log rotation), retry storms, and (in some engines) the loser's subsequent RETRY may silently overwrite the winner's data if the retry logic is naive. The fix — use atomic UPSERT via a single statement: &lt;code&gt;INSERT INTO users (id, name) VALUES (42, 'alice') ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name&lt;/code&gt;. Postgres detects the unique-index conflict &lt;em&gt;inside&lt;/em&gt; the write path (before the constraint check completes) and atomically routes execution to the UPDATE branch. No two-statement race, no error, no retry. The same applies to MySQL (&lt;code&gt;ON DUPLICATE KEY UPDATE&lt;/code&gt;), SQLite (&lt;code&gt;ON CONFLICT DO UPDATE&lt;/code&gt;), and warehouse &lt;code&gt;MERGE INTO&lt;/code&gt; (though warehouses also serialize concurrent MERGE statements). Rule — every backend UPSERT must be a single statement; never a SELECT followed by an INSERT or UPDATE.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I handle SCD Type 2 with MERGE?
&lt;/h3&gt;

&lt;p&gt;SCD Type 2 preserves history — every dimension change creates a new version, with &lt;code&gt;valid_from&lt;/code&gt; and &lt;code&gt;valid_to&lt;/code&gt; timestamps and an &lt;code&gt;is_current&lt;/code&gt; flag. The MERGE pattern is &lt;strong&gt;two-step&lt;/strong&gt;: first close the current version of changed rows, then insert the new versions. Snowflake and BigQuery both express this in two statements (or two MERGE branches):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Step 1: close current versions for changed rows&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;dim_customer&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;valid_to&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;is_current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;FALSE&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;is_current&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;stg_customer&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 2: insert new versions (new customers + changed existing customers)&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;dim_customer&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valid_from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valid_to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s1"&gt;'9999-12-31'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;stg_customer&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;dim_customer&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_current&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
   &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;strong&gt;unique partial index&lt;/strong&gt; &lt;code&gt;(customer_id) WHERE is_current&lt;/code&gt; enforces "one current version per customer" at the storage layer. &lt;strong&gt;dbt's &lt;code&gt;snapshot&lt;/code&gt; macro&lt;/strong&gt; implements this pattern with additional book-keeping (&lt;code&gt;dbt_valid_from&lt;/code&gt;, &lt;code&gt;dbt_valid_to&lt;/code&gt;, &lt;code&gt;dbt_scd_id&lt;/code&gt;). For queries, downstream analytics either filter by &lt;code&gt;WHERE is_current&lt;/code&gt; (Type 1 view) or by &lt;code&gt;WHERE :as_of_date BETWEEN valid_from AND valid_to&lt;/code&gt; (Type 2 view). The idempotency contract still holds — replaying the same batch produces the same final state because the &lt;code&gt;WHERE EXISTS ... AND s.col &amp;lt;&amp;gt; t.col&lt;/code&gt; guard skips unchanged rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practice on PipeCode
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Drill the &lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;SQL practice library →&lt;/a&gt; — 450+ DE-focused questions covering INSERT ... ON CONFLICT, MERGE INTO, and every adjacent pattern.&lt;/li&gt;
&lt;li&gt;Sharpen the join reflex on &lt;a href="https://pipecode.ai/explore/practice/topic/joins" rel="noopener noreferrer"&gt;SQL join drills →&lt;/a&gt; — MERGE is fundamentally a join with row-level actions per matched / unmatched row.&lt;/li&gt;
&lt;li&gt;Layer &lt;a href="https://pipecode.ai/explore/practice/topic/indexing" rel="noopener noreferrer"&gt;SQL indexing drills →&lt;/a&gt; — unique indexes, composite unique constraints, and partial indexes are what MERGE / UPSERT depend on for atomicity.&lt;/li&gt;
&lt;li&gt;Push the difficulty ceiling with &lt;a href="https://pipecode.ai/explore/practice/topic/optimization" rel="noopener noreferrer"&gt;SQL optimization drills →&lt;/a&gt; — read MERGE execution plans, spot the missing partition prune, and rewrite the MERGE for 100× cost reduction.&lt;/li&gt;
&lt;li&gt;Warm up with &lt;a href="https://pipecode.ai/explore/practice/topic/aggregation" rel="noopener noreferrer"&gt;aggregation drills →&lt;/a&gt; — pre-aggregate source before MERGE, dedup with &lt;code&gt;ROW_NUMBER()&lt;/code&gt;, use &lt;code&gt;MAX()&lt;/code&gt; for LWW semantics.&lt;/li&gt;
&lt;li&gt;Layer &lt;a href="https://pipecode.ai/explore/practice/topic/window-functions" rel="noopener noreferrer"&gt;window function drills →&lt;/a&gt; — &lt;code&gt;ROW_NUMBER() OVER (PARTITION BY key ORDER BY updated_at DESC)&lt;/code&gt; is the batch-dedup pattern every MERGE ships with.&lt;/li&gt;
&lt;li&gt;For the broader SQL interview surface, take the &lt;a href="https://pipecode.ai/explore/courses/sql-for-data-engineering-interviews-from-zero-to-faang" rel="noopener noreferrer"&gt;SQL for Data Engineering course →&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/" rel="noopener noreferrer"&gt;Pipecode.ai&lt;/a&gt; is Leetcode for Data Engineering — every `sql merge upsert` recipe above ships with hands-on practice rooms where you type `INSERT ... ON CONFLICT (id) DO UPDATE SET col = EXCLUDED.col` on a live Postgres, migrate the same UPSERT to MySQL's `ON DUPLICATE KEY UPDATE col = new.col` row-alias spelling, translate it to a Snowflake three-branch `MERGE INTO ... USING ... WHEN MATCHED [AND cond] THEN UPDATE / WHEN NOT MATCHED THEN INSERT`, benchmark it against a BigQuery MERGE with partition-prune predicate and `--dry_run` cost gate, and finally dodge the SQL Server MERGE cardinality bug with the CTE + INSERT + UPDATE safe rewrite — the exact 8-engine `postgres on conflict` / `mysql on duplicate key update` / `snowflake merge` fluency that senior DE interviews probe. PipeCode pairs every UPSERT and MERGE concept with 450+ DE-focused problems and a real-time scoring engine, so you never have to wonder whether your idempotent-write answer holds up under a senior interviewer's depth probes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;Practice SQL now →&lt;/a&gt;&lt;br&gt;
&lt;a href="https://pipecode.ai/explore/practice/topic/optimization" rel="noopener noreferrer"&gt;Optimization drills →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>pythons</category>
      <category>sql</category>
      <category>interview</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>SQL EXPLAIN ANALYZE Deep Dive: Reading Execution Plans in Postgres, Snowflake, BigQuery</title>
      <dc:creator>Gowtham Potureddi</dc:creator>
      <pubDate>Sat, 11 Jul 2026 16:26:34 +0000</pubDate>
      <link>https://dev.to/gowthampotureddi/sql-explain-analyze-deep-dive-reading-execution-plans-in-postgres-snowflake-bigquery-3ico</link>
      <guid>https://dev.to/gowthampotureddi/sql-explain-analyze-deep-dive-reading-execution-plans-in-postgres-snowflake-bigquery-3ico</guid>
      <description>&lt;p&gt;&lt;strong&gt;&lt;code&gt;sql explain analyze&lt;/code&gt;&lt;/strong&gt; is the primitive every senior data engineer, analytics engineer, and DBA reaches for at 3 a.m. when a dashboard query that used to run in 200 milliseconds suddenly takes 40 seconds — and it is the primitive most engineers can &lt;em&gt;type&lt;/em&gt; on day one but only a small minority can actually &lt;em&gt;read&lt;/em&gt; on day one thousand. The gap between "I know the keyword" and "I can look at a plan artefact and name the fix out loud in one breath" is the gap between a mid-level candidate who gets nervous when the interviewer opens the plan tab and a senior candidate who takes the mouse, points at the hot node, and says "the estimate is thirty times the actual — run &lt;code&gt;ANALYZE&lt;/code&gt;, then rewrite the join." This guide is the honest tour of what actually happens inside the planner when you prefix a query with &lt;code&gt;EXPLAIN (ANALYZE, BUFFERS, VERBOSE)&lt;/code&gt; — how the four numbers per node compose into a plan tree, why &lt;code&gt;cost=&lt;/code&gt; is a made-up unit while &lt;code&gt;actual time=&lt;/code&gt; is wall clock, how Snowflake's Query Profile, BigQuery's execution graph, and SQL Server's actual execution plan translate the same ideas into different artefacts, and how the same five-step reading discipline turns any of them into a concrete fix.&lt;/p&gt;

&lt;p&gt;The tour walks the four engines you have to keep straight in 2026 — Postgres &lt;code&gt;EXPLAIN (ANALYZE, BUFFERS, VERBOSE, FORMAT TEXT)&lt;/code&gt; with its cost / rows / loops / actual-time-per-node grid and the &lt;code&gt;BUFFERS&lt;/code&gt; clause that surfaces the shared-hit / shared-read / temp-read triangle where spilling hides, the Snowflake Query Profile with its operator cards, micro-partition pruning percent, and local-vs-remote disk spill panel that decides warehouse sizing, the BigQuery execution graph with its stage-based DAG, slot-milliseconds accounting, &lt;code&gt;--dry_run&lt;/code&gt; cost estimator, and &lt;code&gt;INFORMATION_SCHEMA.JOBS&lt;/code&gt; audit trail, and finally the SQL Server actual execution plan XML with &lt;code&gt;SET STATISTICS TIME/IO ON&lt;/code&gt; and the graphical SSMS view. Each section ships a teaching block followed by a Solution-Tail interview answer — code, a step-by-step trace, an output table, then a concept-by-concept breakdown of why it works — so you leave with the mental model, the syntax, and the senior read-strategy your next interviewer is listening for.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr7zzcolqw97oz6hzcvxu.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr7zzcolqw97oz6hzcvxu.jpeg" alt="PipeCode blog header for SQL EXPLAIN ANALYZE deep dive — bold white headline 'EXPLAIN ANALYZE' with subtitle 'Reading Execution Plans in Postgres, Snowflake, BigQuery' and a stylised scene showing a three-column plan tree with a Postgres node, a Snowflake operator card, and a BigQuery stage graph on a dark gradient with purple, green, orange, and blue accents and a small pipecode.ai attribution." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you want &lt;strong&gt;hands-on reps&lt;/strong&gt; immediately after reading, drill the &lt;a href="https://pipecode.ai/explore/practice/topic/optimization" rel="noopener noreferrer"&gt;SQL optimization practice library →&lt;/a&gt; for plan-reading and rewrite drills, warm up the index reflex with the &lt;a href="https://pipecode.ai/explore/practice/topic/indexing" rel="noopener noreferrer"&gt;SQL indexing room →&lt;/a&gt;, and layer the broader &lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;SQL practice surface →&lt;/a&gt; covering 450+ DE-focused problems that pair every plan artefact with a concrete rewrite.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;On this page&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why execution plans matter in 2026&lt;/li&gt;
&lt;li&gt;Postgres EXPLAIN ANALYZE anatomy&lt;/li&gt;
&lt;li&gt;Snowflake QUERY_PROFILE&lt;/li&gt;
&lt;li&gt;BigQuery execution graph&lt;/li&gt;
&lt;li&gt;Dialect matrix + 5-step reading strategy&lt;/li&gt;
&lt;li&gt;Cheat sheet — plan-reading recipe list&lt;/li&gt;
&lt;li&gt;Frequently asked questions&lt;/li&gt;
&lt;li&gt;Practice on PipeCode&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1. Why execution plans matter in 2026
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The &lt;code&gt;sql query optimizer&lt;/code&gt; mental model — cost-based optimisation, statistics, cardinality estimation, and why every senior read-strategy starts with a plan
&lt;/h3&gt;

&lt;p&gt;The one-sentence invariant: &lt;strong&gt;an execution plan is the planner's compiled decision tree for how to physically execute a logical SQL statement — which access method for each table, which join algorithm for each join, which order to evaluate joins in, whether to sort, whether to hash, whether to spill, whether to parallelise — and reading it is the fastest way to translate "this query is slow" into "here is the exact one-line fix"&lt;/strong&gt;. Every engine ships an optimiser; every optimiser makes decisions based on table statistics and a cost model; every cost model is imperfect; and every imperfect cost model produces a bad plan on some query somewhere in your product every week. The senior discipline is not to memorise a hundred rewrites — it is to open the plan, find the one node where estimate diverges from actual, and let that number tell you what to fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The planner mental model in five layers.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Parser.&lt;/strong&gt; Turns the SQL string into an abstract syntax tree. This step never fails silently — a syntax error surfaces as a parse error. Nothing interesting to read at this layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rewriter.&lt;/strong&gt; Applies rule-based transformations — view expansion, subquery flattening, predicate pushdown into the CTE / subquery boundary, constant folding. On Postgres this is the &lt;code&gt;pg_rewrite&lt;/code&gt; layer; on Snowflake it is folded into the optimiser; on BigQuery it happens before stage assignment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost-based optimiser (CBO).&lt;/strong&gt; Considers many equivalent plan shapes, assigns a cost to each based on &lt;strong&gt;table statistics&lt;/strong&gt; (row counts, distinct values, most common values, histograms, correlation coefficient), and picks the cheapest. This is where "the planner got it wrong" usually happens — the stats are stale, the histogram is coarse, the correlation between two columns is off, or the CBO does not know about a functional dependency your data has.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Executor.&lt;/strong&gt; Walks the chosen plan tree and returns tuples up the pipeline. Modern executors are pipelined (results stream up the tree) rather than materialised (each node writes to a temp table). The &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; output shows the &lt;em&gt;actual&lt;/em&gt; row count and wall clock at each node — the executor's ground truth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Statistics collector.&lt;/strong&gt; A background job on every engine that samples tables, updates histograms, and feeds the CBO on the next planning cycle. On Postgres — &lt;code&gt;ANALYZE&lt;/code&gt; or &lt;code&gt;autovacuum&lt;/code&gt;. On Snowflake — automatic and invisible. On BigQuery — table metadata refreshed continuously.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cost units vs actual time — the two axes of every plan node.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost is a made-up unit.&lt;/strong&gt; In Postgres, cost is measured in "arbitrary planner units" — a &lt;code&gt;Seq Scan&lt;/code&gt; on a 1-row table costs about 0.01 units; a &lt;code&gt;Hash Join&lt;/code&gt; on a million rows might cost 250,000 units. The unit does not correspond to milliseconds, CPU cycles, or bytes. It is only meaningful &lt;em&gt;relative&lt;/em&gt; to other plan alternatives during optimisation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Actual time is wall clock.&lt;/strong&gt; &lt;code&gt;actual time=0.017..0.019&lt;/code&gt; means "the first row from this node was ready at 0.017 ms after query start; the last row at 0.019 ms." This is real, measurable, comparable across queries. Every senior read starts here — cost is the optimiser's guess, actual time is what happened.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The estimate-vs-actual gap is the smoking gun.&lt;/strong&gt; If a node has &lt;code&gt;rows=100 loops=1&lt;/code&gt; in the plan header but &lt;code&gt;actual rows=100000 loops=1&lt;/code&gt;, the optimiser was off by a thousand times — a bad plan is almost guaranteed. Fix — run &lt;code&gt;ANALYZE&lt;/code&gt;, check for stale stats, or add extended statistics on correlated columns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost is only comparable within a single plan.&lt;/strong&gt; You cannot compare cost 12,500 for query A against cost 8,700 for query B on the same table — the units are internal to the planner, not portable across queries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What senior interviewers actually probe when they open the plan tab.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Do you know how to read the plan tree?&lt;/strong&gt; Bottom-up, inner-most first. The &lt;code&gt;Limit&lt;/code&gt; at the top is the last node executed; the &lt;code&gt;Seq Scan&lt;/code&gt; / &lt;code&gt;Index Scan&lt;/code&gt; at the bottom is the first. Candidates who read top-down miss the hot leaf every time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do you know the node types?&lt;/strong&gt; &lt;code&gt;Seq Scan&lt;/code&gt; = full table scan. &lt;code&gt;Index Scan&lt;/code&gt; = seek + heap fetch. &lt;code&gt;Bitmap Heap Scan&lt;/code&gt; = index-driven heap fetch with bitmap OR. &lt;code&gt;Hash Join&lt;/code&gt; = build hash table on smaller side, probe with larger. &lt;code&gt;Merge Join&lt;/code&gt; = zip two pre-sorted streams. &lt;code&gt;Nested Loop&lt;/code&gt; = outer × inner. &lt;code&gt;Sort&lt;/code&gt; = sort operator (may spill). &lt;code&gt;Hash Aggregate&lt;/code&gt; = hash-based &lt;code&gt;GROUP BY&lt;/code&gt;. &lt;code&gt;Gather&lt;/code&gt; = parallel-worker collector.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do you know when the plan is bad?&lt;/strong&gt; Estimate off by 10× or more. &lt;code&gt;Buffers: temp read&lt;/code&gt; non-zero (spilling to disk). &lt;code&gt;Nested Loop&lt;/code&gt; on two large tables. &lt;code&gt;Seq Scan&lt;/code&gt; where an index would work. &lt;code&gt;Sort → Hash Aggregate&lt;/code&gt; where the &lt;code&gt;Sort&lt;/code&gt; is doing all the work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do you know the fix?&lt;/strong&gt; Run &lt;code&gt;ANALYZE&lt;/code&gt;. Add a covering index. Rewrite a correlated subquery into a &lt;code&gt;JOIN&lt;/code&gt;. Add a &lt;code&gt;LATERAL&lt;/code&gt; where a scalar subquery loops. Split a &lt;code&gt;WITH RECURSIVE&lt;/code&gt; into a materialised temp table. Increase &lt;code&gt;work_mem&lt;/code&gt; for a specific session. Add &lt;code&gt;pg_hint_plan&lt;/code&gt; hints in exceptional cases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do you know the trade-offs?&lt;/strong&gt; Adding an index speeds reads but slows writes. Increasing &lt;code&gt;work_mem&lt;/code&gt; speeds sorts but risks OOM under concurrency. Materialising a CTE eats memory but avoids a recompute. Hints lock the plan — great for known cases, terrible when data shifts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The reading discipline — bottom-up, node-by-node, cost-vs-actual ratio.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Step 1 — bottom-up.&lt;/strong&gt; Find the leaf nodes (scans, &lt;code&gt;Values&lt;/code&gt; clauses, function-returning-set nodes). Read their &lt;code&gt;actual rows&lt;/code&gt; and &lt;code&gt;actual time&lt;/code&gt;. This is what the executor read from disk (or memory).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 2 — cost-vs-actual ratio.&lt;/strong&gt; For each node, compare &lt;code&gt;rows=&lt;/code&gt; (estimate) to &lt;code&gt;actual rows=&lt;/code&gt;. A ratio within 2× is fine. 10× is worth investigating. 100× is almost always a bad plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 3 — hot node.&lt;/strong&gt; Find the node whose &lt;code&gt;actual time&lt;/code&gt; is the largest fraction of total wall clock. This is the node to optimise. Speeding up a node that takes 0.1 ms of a 40 s query is pointless.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 4 — spill / shuffle / parallelism.&lt;/strong&gt; Check for &lt;code&gt;Buffers: temp read&lt;/code&gt; (spill), &lt;code&gt;Sort Method: external merge&lt;/code&gt; (bigger spill), &lt;code&gt;Workers Launched: 0&lt;/code&gt; (parallelism disabled), &lt;code&gt;Rows Removed by Filter: 999999&lt;/code&gt; (post-filter waste). Each is a specific fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 5 — the one-line rewrite.&lt;/strong&gt; Based on the hot node's failure mode, name the fix out loud — add index, rewrite join, increase memory, run ANALYZE, add hint. Senior candidates land on the fix within thirty seconds of opening the plan.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why interviewers open plan tabs in senior loops.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's a fluency check on the executor mental model.&lt;/strong&gt; Anyone can type &lt;code&gt;EXPLAIN&lt;/code&gt;; the senior signal is being able to look at "Hash Join (cost=12500..37500 rows=1000 width=64) (actual time=45..190 rows=1000000 loops=1)" and say "estimate was a thousand rows, actual was a million — the join is spilling to disk and the plan is wrong" in one breath.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's a fluency check on the storage model.&lt;/strong&gt; Postgres reads pages; Snowflake reads micro-partitions; BigQuery reads columnar files. The &lt;code&gt;Buffers&lt;/code&gt; line, the &lt;code&gt;bytes scanned&lt;/code&gt; line, the &lt;code&gt;Partitions scanned&lt;/code&gt; line — each surfaces the storage cost that differs per engine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's a fluency check on cost.&lt;/strong&gt; On BigQuery, wrong plan = wrong bill ($5/TB scanned on-demand). On Snowflake, wrong plan = wrong warehouse size (2× per tier). On Postgres, wrong plan = wrong tail latency (100 ms → 40 s). Senior candidates can quantify the cost of a bad plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's a fluency check on system design.&lt;/strong&gt; "Design a Postgres schema to support this dashboard" — great answers include the index plan (which composite indexes, in which order), the analyse cadence, and how the cost model reacts to skew. The plan surface &lt;em&gt;is&lt;/em&gt; the system design surface.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's a fluency check on debugging discipline.&lt;/strong&gt; "The query got 100× slower after a data load — walk me through your diagnosis." Every senior answer starts with &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; on the slow and fast versions and diffs the plan trees.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Worked example — the day-one &lt;code&gt;EXPLAIN&lt;/code&gt; on a mystery-slow query
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The archetype: a dashboard query that used to run in 200 ms now takes 40 seconds. The junior instinct is to add an index; the senior instinct is to open the plan and let the numbers name the fix. The single most useful &lt;code&gt;EXPLAIN&lt;/code&gt; variant on Postgres is &lt;code&gt;EXPLAIN (ANALYZE, BUFFERS, VERBOSE, FORMAT TEXT)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given the query below on a Postgres 15 database, prefix it with the right &lt;code&gt;EXPLAIN&lt;/code&gt; variant to surface plan shape, actual timings, buffer accounting, and node targeting. Name the four things you would read first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- The mystery-slow query&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;lifetime_value&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;country&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'US'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'30 days'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;lifetime_value&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUFFERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;VERBOSE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FORMAT&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;lifetime_value&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;country&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'US'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'30 days'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;lifetime_value&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;EXPLAIN&lt;/code&gt; alone prints the &lt;em&gt;planned&lt;/em&gt; tree with estimates only — no query executes. Use this when the query is prohibitively expensive to actually run.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ANALYZE&lt;/code&gt; runs the query and returns real &lt;code&gt;actual time&lt;/code&gt; and &lt;code&gt;actual rows&lt;/code&gt; per node. This is what you want 95% of the time — the plan the executor actually walked.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;BUFFERS&lt;/code&gt; adds the &lt;code&gt;shared hit / shared read / temp read&lt;/code&gt; triangle per node — cache hits, cold buffer reads from disk, and temp writes (spill). Non-zero &lt;code&gt;temp read&lt;/code&gt; is a bright-red flag.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;VERBOSE&lt;/code&gt; adds the column list and expression detail for each node — expensive to eyeball but crucial when you need to know exactly which columns are projected at each layer.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FORMAT TEXT&lt;/code&gt; (the default) is the classic indented tree. &lt;code&gt;FORMAT JSON&lt;/code&gt; is machine-readable for CI regression tests; &lt;code&gt;FORMAT YAML&lt;/code&gt; is a middle ground.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output&lt;/strong&gt; (elided).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Limit  (cost=250.31..250.56 rows=100 width=16) (actual time=42.512..42.548 rows=100 loops=1)
  Buffers: shared hit=1234, read=456, temp read=42 written=42
  -&amp;gt;  Sort  (cost=250.31..252.81 rows=1000 width=16) (actual time=42.510..42.520 rows=100 loops=1)
        Sort Key: (sum(o.total)) DESC
        Sort Method: top-N heapsort  Memory: 33kB
        ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For 95% of interview-flow diagnosis and on-call debugging, &lt;code&gt;EXPLAIN (ANALYZE, BUFFERS)&lt;/code&gt; is the right variant. Add &lt;code&gt;VERBOSE&lt;/code&gt; when you suspect a projection issue; add &lt;code&gt;FORMAT JSON&lt;/code&gt; when you want to diff plans across two queries programmatically.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — cost units are not milliseconds (the day-two lesson)
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The most common junior misconception: reading &lt;code&gt;cost=12500..37500&lt;/code&gt; and treating those as milliseconds. They are not. They are internal planner units used only to rank plan alternatives. The single number that translates to real time is &lt;code&gt;actual time=&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given the plan fragment below, which numbers are milliseconds and which are made-up units? What is the total wall-clock time of the query?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Limit  (cost=12500.31..12500.56 rows=100 width=16) (actual time=42.512..42.548 rows=100 loops=1)
  -&amp;gt;  Sort  (cost=12500.31..12502.81 rows=1000 width=16) (actual time=42.510..42.520 rows=100 loops=1)
        -&amp;gt;  HashAggregate  (cost=12495.00..12497.50 rows=1000 width=16) (actual time=41.900..42.400 rows=850 loops=1)
              -&amp;gt;  Hash Join  (cost=1250.00..12480.00 rows=100000 width=12) (actual time=25.100..40.500 rows=98000 loops=1)
                    ...
Planning Time: 0.150 ms
Execution Time: 42.702 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt; (No code — a plan-reading exercise.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;made-up units: cost=..
milliseconds:  actual time=..
row counts:    rows= (estimate) and actual rows= (real)
loops:         actual loops=N — number of times this node was executed
totals:        Planning Time (ms) + Execution Time (ms)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;cost=12500.31..12500.56&lt;/code&gt; — the two numbers are the estimated startup cost and total cost, in arbitrary planner units. Startup cost is what the node pays before emitting its first row; total cost is what it pays to emit all rows.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;actual time=42.512..42.548&lt;/code&gt; — the two numbers are the real wall-clock times for the first and last row emitted by this node, in milliseconds. If &lt;code&gt;loops &amp;gt; 1&lt;/code&gt;, these are the &lt;em&gt;per-loop&lt;/em&gt; averages, not the total.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rows=100&lt;/code&gt; — the optimiser's estimate. &lt;code&gt;actual rows=100&lt;/code&gt; — the real count. If these differ by 10×, the plan is likely suboptimal.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;loops=1&lt;/code&gt; — this node executed once. Nodes inside a nested loop, function scan, or subquery may execute many times; in that case wall-clock time = &lt;code&gt;actual time (per loop) × loops&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Execution Time: 42.702 ms&lt;/code&gt; — the true wall clock for the query. &lt;code&gt;Planning Time&lt;/code&gt; — how long the optimiser took to plan (usually sub-millisecond; large for many-JOIN queries).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt; Wall-clock total = 42.702 ms. The cost numbers (12500, 12480) are not milliseconds — do not multiply them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Read &lt;code&gt;actual time&lt;/code&gt; for wall clock. Read &lt;code&gt;cost&lt;/code&gt; only to compare plan alternatives when explaining why the optimiser chose plan A over plan B. Never quote cost numbers to a business stakeholder.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — bottom-up reading discipline
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The plan tree in &lt;code&gt;FORMAT TEXT&lt;/code&gt; is nested — inner-most first. Execution walks bottom-up: leaf nodes (scans) produce rows, they feed intermediate nodes (joins, aggregates), which feed the top node (&lt;code&gt;Limit&lt;/code&gt;, &lt;code&gt;Sort&lt;/code&gt;). Reading top-down misses which leaf is doing the work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given the plan below, which node is the bottleneck? Which node should you optimise? Give the reasoning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Limit  (actual time=1200.5..1200.6 rows=100 loops=1)
  -&amp;gt;  Sort  (actual time=1200.4..1200.5 rows=100 loops=1)
        -&amp;gt;  HashAggregate  (actual time=1198.3..1199.1 rows=850 loops=1)
              -&amp;gt;  Hash Join  (actual time=100.5..1195.4 rows=98000 loops=1)
                    -&amp;gt;  Seq Scan on orders o  (actual time=99.0..1150.0 rows=10000000 loops=1)
                          Filter: (created_at &amp;gt;= now() - '30 days'::interval)
                          Rows Removed by Filter: 90000000
                    -&amp;gt;  Hash  (actual time=0.5..0.5 rows=1000 loops=1)
                          -&amp;gt;  Seq Scan on customers c  (actual time=0.1..0.4 rows=1000 loops=1)
                                Filter: (country = 'US'::text)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt; (No code — a plan-reading exercise.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read bottom-up. The two &lt;code&gt;Seq Scan&lt;/code&gt; leaves execute first. &lt;code&gt;Seq Scan on customers c&lt;/code&gt; — 0.4 ms — cheap. &lt;code&gt;Seq Scan on orders o&lt;/code&gt; — 1150 ms — this is the hot node.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Rows Removed by Filter: 90000000&lt;/code&gt; on the orders scan — the engine read 100 M rows and threw away 90 M of them via the &lt;code&gt;created_at &amp;gt;= NOW() - INTERVAL '30 days'&lt;/code&gt; filter. That means no index on &lt;code&gt;created_at&lt;/code&gt; exists, or the filter is not sargable.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;Hash Join&lt;/code&gt; at &lt;code&gt;100.5..1195.4&lt;/code&gt; inherits the cost of the orders scan (~1095 ms of the join time is upstream from the scan). The join itself is cheap.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;HashAggregate&lt;/code&gt; and &lt;code&gt;Sort&lt;/code&gt; are microseconds — they operate on 100 K joined rows, not 100 M.&lt;/li&gt;
&lt;li&gt;The fix is at the leaf — add an index on &lt;code&gt;orders(created_at)&lt;/code&gt;. The join, aggregate, and sort are already optimal.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt; The bottleneck is &lt;code&gt;Seq Scan on orders o&lt;/code&gt;. Fix: &lt;code&gt;CREATE INDEX ON orders(created_at)&lt;/code&gt;. Estimated speed-up: 100 M reads → ~1 M reads = ~100× — from ~1200 ms to ~15 ms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Bottom-up. Always. The leaf that dominates wall clock is the leaf to optimise. Top-level nodes rarely need attention unless they explicitly spill.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Common beginner mistakes&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reading the plan top-down — missing the hot leaf.&lt;/li&gt;
&lt;li&gt;Treating &lt;code&gt;cost=&lt;/code&gt; as milliseconds — confusing internal units for wall clock.&lt;/li&gt;
&lt;li&gt;Ignoring &lt;code&gt;Rows Removed by Filter&lt;/code&gt; — a filter that discards 90% of rows is telling you which index to add.&lt;/li&gt;
&lt;li&gt;Skipping &lt;code&gt;BUFFERS&lt;/code&gt; — the temp-read line is the smoking gun for spilling.&lt;/li&gt;
&lt;li&gt;Confusing &lt;code&gt;loops=1&lt;/code&gt; with &lt;code&gt;loops=N&lt;/code&gt; — inside a nested loop, per-loop times must be multiplied by &lt;code&gt;loops&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  SQL interview question on when to reach for &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;A senior interviewer often opens with: "You're on-call and a dashboard query for the CFO went from 200 ms to 40 seconds after last night's data load. Walk me through your first ten minutes of diagnosis. Which &lt;code&gt;EXPLAIN&lt;/code&gt; variant do you run, which numbers do you read first, and what are the top three fixes you would try in order?"&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using &lt;code&gt;EXPLAIN (ANALYZE, BUFFERS)&lt;/code&gt; on the slow and fast versions, diffing plan trees
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Step 1 — capture the slow plan&lt;/span&gt;
&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUFFERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;VERBOSE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FORMAT&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2026-06-10'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 2 — capture the fast plan by rolling back one week&lt;/span&gt;
&lt;span class="c1"&gt;-- (via a read replica pinned to yesterday's snapshot, or a staging DB)&lt;/span&gt;
&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUFFERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;VERBOSE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FORMAT&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2026-06-03'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 3 — refresh statistics if the slow plan estimate is off by 10× or more&lt;/span&gt;
&lt;span class="k"&gt;ANALYZE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 4 — re-run the slow plan&lt;/span&gt;
&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUFFERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2026-06-10'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 5 — if still slow, check for a missing index by hunting Rows Removed by Filter&lt;/span&gt;
&lt;span class="c1"&gt;-- Fix: CREATE INDEX ON orders(created_at) or a composite (customer_id, created_at DESC)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Reads&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Prefix slow query with &lt;code&gt;EXPLAIN (ANALYZE, BUFFERS)&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Plan tree with cost/actual/loops/buffers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Compare to fast-version plan on replica&lt;/td&gt;
&lt;td&gt;Diff shows which node's cost or rows changed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Look for &lt;code&gt;rows=&lt;/code&gt; vs &lt;code&gt;actual rows=&lt;/code&gt; gap ≥ 10×&lt;/td&gt;
&lt;td&gt;Indicates stale statistics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Run &lt;code&gt;ANALYZE orders&lt;/code&gt; to refresh statistics&lt;/td&gt;
&lt;td&gt;Optimiser now has fresh histograms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Re-&lt;code&gt;EXPLAIN&lt;/code&gt; — if plan is now fast, root cause was stale stats&lt;/td&gt;
&lt;td&gt;Fixed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;If still slow, look for &lt;code&gt;Rows Removed by Filter: NNN&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Missing index — add one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Verify final plan is &lt;code&gt;Index Scan&lt;/code&gt; not &lt;code&gt;Seq Scan&lt;/code&gt; at the hot leaf&lt;/td&gt;
&lt;td&gt;Ship&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Diagnosis outcome&lt;/th&gt;
&lt;th&gt;Root cause&lt;/th&gt;
&lt;th&gt;One-line fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Estimate rows 10× &amp;lt; actual&lt;/td&gt;
&lt;td&gt;Stale statistics&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ANALYZE orders;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Rows Removed by Filter&lt;/code&gt; in the millions&lt;/td&gt;
&lt;td&gt;Missing index&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CREATE INDEX ON orders(created_at);&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Buffers: temp read=NNN&lt;/code&gt; non-zero&lt;/td&gt;
&lt;td&gt;Sort or hash spill&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SET work_mem = '128MB';&lt;/code&gt; (session)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Nested Loop&lt;/code&gt; on two large tables&lt;/td&gt;
&lt;td&gt;Bad join estimate&lt;/td&gt;
&lt;td&gt;Rewrite join, add composite index, or hint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Seq Scan&lt;/code&gt; on a large table with cheap filter&lt;/td&gt;
&lt;td&gt;Selectivity &amp;lt; 1% but no index&lt;/td&gt;
&lt;td&gt;Add partial index, or composite index&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Diff two plans&lt;/strong&gt;&lt;/strong&gt; — a slow-and-a-fast plan on the same query shape (yesterday vs today, prod vs replica) instantly localises the regression. If &lt;code&gt;Hash Join&lt;/code&gt; was cheap yesterday and expensive today, the stats on the join columns are stale or the data distribution shifted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;&lt;code&gt;ANALYZE&lt;/code&gt; fixes stale statistics&lt;/strong&gt;&lt;/strong&gt; — after a big data load, the planner's histograms and MCV lists are out of date. Running &lt;code&gt;ANALYZE table&lt;/code&gt; re-samples and refreshes them; the next &lt;code&gt;EXPLAIN&lt;/code&gt; on the same query often shows a completely different, correctly-costed plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;&lt;code&gt;Rows Removed by Filter&lt;/code&gt; names the missing index&lt;/strong&gt;&lt;/strong&gt; — the executor is telling you "I read N rows, threw away most of them via this filter." That filter &lt;em&gt;should&lt;/em&gt; be an index seek instead of a scan. Add the index and the filter becomes an &lt;code&gt;Index Cond&lt;/code&gt; — zero rows discarded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;&lt;code&gt;BUFFERS: temp read&lt;/code&gt; is spilling&lt;/strong&gt;&lt;/strong&gt; — the planner allocated a sort or hash that outgrew &lt;code&gt;work_mem&lt;/code&gt;. Two fixes: rewrite the query to sort or aggregate less (&lt;code&gt;LIMIT&lt;/code&gt; earlier, smaller GROUP BY), or bump &lt;code&gt;work_mem&lt;/code&gt; for this session. Never bump it globally — under concurrency, every session gets its own copy and you run out of RAM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;EXPLAIN&lt;/code&gt; alone is free (planning cost only). &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; runs the query — expensive if the query is slow. On production, prefer &lt;code&gt;EXPLAIN&lt;/code&gt; alone for exploratory diagnosis; use &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; when you can afford the extra wall clock or when the query returns quickly enough. The read discipline itself is &lt;code&gt;O(depth of plan tree)&lt;/code&gt; — a five-node plan reads in five seconds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — optimization&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL optimization drills&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/optimization" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — indexing&lt;/span&gt;
&lt;strong&gt;SQL indexing problems&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/indexing" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  2. Postgres EXPLAIN ANALYZE anatomy
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;postgres explain analyze&lt;/code&gt; — the four numbers per node, the six major node types, and how &lt;code&gt;BUFFERS&lt;/code&gt; surfaces the spill triangle
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;every Postgres plan node emits four numbers — &lt;code&gt;cost&lt;/code&gt; (planner units), &lt;code&gt;rows&lt;/code&gt; (estimate), &lt;code&gt;actual time&lt;/code&gt; (wall clock), and &lt;code&gt;loops&lt;/code&gt; (executions) — plus a variable-length &lt;code&gt;BUFFERS&lt;/code&gt; triangle of &lt;code&gt;shared hit / shared read / temp read&lt;/code&gt; that reveals whether the node lived in cache, hit cold buffers, or spilled to disk&lt;/strong&gt;. Once you memorise the four numbers per node and the six major node types, you can read almost any Postgres plan in one pass.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6xegodbmm9spt8l7n6vr.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6xegodbmm9spt8l7n6vr.jpeg" alt="Visual diagram of Postgres EXPLAIN ANALYZE anatomy — left a plan tree card with three nested nodes annotated with cost/rows/loops/actual time, centre a BUFFERS breakdown showing shared hit vs shared read vs temp read, right a node-type legend card mapping Seq Scan / Index Scan / Bitmap Heap Scan / Hash Join / Merge Join / Sort with colour ribbons; on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Slot 1 — the four numbers per node.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;cost=startup..total&lt;/code&gt;&lt;/strong&gt; — planner units. Startup cost is what the node pays before emitting its first row (sort must complete before emit; hash must build before probe). Total cost is the price to emit all rows. Ratio &lt;code&gt;total / startup&lt;/code&gt; reveals whether the node is pipelined (near 1:1) or blocking (large gap).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;rows=N&lt;/code&gt;&lt;/strong&gt; — the planner's estimate. Comes from table statistics (&lt;code&gt;pg_stats&lt;/code&gt;) and the selectivity of the predicates above. Skewed columns without extended statistics produce bad estimates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;actual time=first..last&lt;/code&gt;&lt;/strong&gt; — wall clock (ms). First is when the first row was ready; last is when the last row was ready. If loops &amp;gt; 1, both are per-loop averages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;actual rows=N&lt;/code&gt;&lt;/strong&gt; — the real count. If &lt;code&gt;rows=100&lt;/code&gt; but &lt;code&gt;actual rows=100000&lt;/code&gt;, the optimiser was off by 1000× — almost certainly a bad plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;loops=N&lt;/code&gt;&lt;/strong&gt; — how many times the executor ran this node. &lt;code&gt;loops=1&lt;/code&gt; for most; &lt;code&gt;loops=N&lt;/code&gt; for nodes inside a nested loop or a subquery per outer row. Wall-clock cost of the node = &lt;code&gt;actual time × loops&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 2 — the six major node types you must recognise on sight.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Seq Scan&lt;/code&gt;&lt;/strong&gt; — full table scan. Reads every row in the table, applies filters row-by-row. Fast on small tables; catastrophic on big tables when a filter has good selectivity (should have been an index).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Index Scan&lt;/code&gt;&lt;/strong&gt; — btree seek + heap fetch. Uses an index to locate rows, then reads the heap page. Best when selectivity is high (&amp;lt; 1% of table) and the projected columns are mostly in the index or need heap fetch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Index Only Scan&lt;/code&gt;&lt;/strong&gt; — btree seek without heap fetch. Available when all projected columns are covered by the index. Fastest read path — often 10× faster than &lt;code&gt;Index Scan&lt;/code&gt; because it skips heap I/O.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Bitmap Heap Scan&lt;/code&gt;&lt;/strong&gt; — index-driven with heap fetch, but the executor first builds a bitmap of matching heap pages, then reads them in sorted order. Fast on medium-selectivity filters (1–10% of table) — better than &lt;code&gt;Seq Scan&lt;/code&gt; because it skips non-matching pages, better than plain &lt;code&gt;Index Scan&lt;/code&gt; because it batches heap I/O.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Hash Join&lt;/code&gt;&lt;/strong&gt; — build a hash table on the smaller side, probe with the larger. &lt;code&gt;O(build_side_rows + probe_side_rows)&lt;/code&gt;. Great for large tables with no useful sort order; may spill if the build side exceeds &lt;code&gt;work_mem&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Merge Join&lt;/code&gt;&lt;/strong&gt; — zip two pre-sorted streams. &lt;code&gt;O(N + M)&lt;/code&gt; after sort, or &lt;code&gt;O(N + M)&lt;/code&gt; if both sides come pre-sorted from an index. Great when input is already sorted; expensive if you have to explicitly sort first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Nested Loop&lt;/code&gt;&lt;/strong&gt; — outer × inner: for each outer row, execute the inner. &lt;code&gt;O(outer × inner)&lt;/code&gt;. Fine when outer is tiny; catastrophic when outer is millions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Sort&lt;/code&gt;&lt;/strong&gt; — external sort operator. Cheap when input fits &lt;code&gt;work_mem&lt;/code&gt;; expensive when it spills to &lt;code&gt;temp read&lt;/code&gt;. &lt;code&gt;Sort Method: quicksort&lt;/code&gt; = in-memory; &lt;code&gt;Sort Method: top-N heapsort&lt;/code&gt; = smart bounded sort for &lt;code&gt;ORDER BY ... LIMIT N&lt;/code&gt;; &lt;code&gt;Sort Method: external merge&lt;/code&gt; = spilled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Hash Aggregate&lt;/code&gt; / &lt;code&gt;Group Aggregate&lt;/code&gt;&lt;/strong&gt; — hash-based or sort-based &lt;code&gt;GROUP BY&lt;/code&gt;. &lt;code&gt;Hash Aggregate&lt;/code&gt; builds a hash on group keys; &lt;code&gt;Group Aggregate&lt;/code&gt; requires pre-sorted input. &lt;code&gt;Hash Aggregate&lt;/code&gt; may spill; &lt;code&gt;Group Aggregate&lt;/code&gt; never does but requires a &lt;code&gt;Sort&lt;/code&gt; upstream.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Gather&lt;/code&gt; / &lt;code&gt;Gather Merge&lt;/code&gt;&lt;/strong&gt; — parallel worker collector. &lt;code&gt;Gather&lt;/code&gt; collects rows in arbitrary order; &lt;code&gt;Gather Merge&lt;/code&gt; preserves sort order from workers. Presence of these means the planner chose parallel execution — &lt;code&gt;parallel_workers&lt;/code&gt; and &lt;code&gt;max_parallel_workers_per_gather&lt;/code&gt; control this.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 3 — the &lt;code&gt;BUFFERS&lt;/code&gt; triangle: &lt;code&gt;shared hit / shared read / temp read&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;shared hit=N&lt;/code&gt;&lt;/strong&gt; — pages served from the buffer cache (shared buffers). Fast — RAM read, no disk I/O.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;shared read=N&lt;/code&gt;&lt;/strong&gt; — pages read from disk into the buffer cache. Slower than hit; each read is ~100 μs on SSD, ~5 ms on spinning disk. High shared read = cold cache; may warm up on subsequent runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;shared dirtied=N&lt;/code&gt;&lt;/strong&gt; — pages modified by the query (usually only for DML). Ignore for read-only diagnosis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;temp read=N&lt;/code&gt; / &lt;code&gt;temp written=N&lt;/code&gt;&lt;/strong&gt; — pages the executor spilled to disk because a &lt;code&gt;Sort&lt;/code&gt;, &lt;code&gt;Hash Join&lt;/code&gt;, or &lt;code&gt;Hash Aggregate&lt;/code&gt; overflowed &lt;code&gt;work_mem&lt;/code&gt;. &lt;strong&gt;This is the spill signal.&lt;/strong&gt; Any non-zero temp read means the query is thrashing — fix is either query rewrite (limit earlier, smaller GROUP BY) or &lt;code&gt;SET work_mem = '128MB'&lt;/code&gt; for the session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;shared read / (shared hit + shared read)&lt;/code&gt;&lt;/strong&gt; — the cache miss rate. Under 5% is fine; over 50% means the working set doesn't fit in &lt;code&gt;shared_buffers&lt;/code&gt; or the buffer cache is being churned.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 4 — reading a plan bottom-up in five moves.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Move 1 — identify the leaves.&lt;/strong&gt; Every &lt;code&gt;Seq Scan&lt;/code&gt;, &lt;code&gt;Index Scan&lt;/code&gt;, &lt;code&gt;Bitmap Heap Scan&lt;/code&gt;, &lt;code&gt;Values Scan&lt;/code&gt;, &lt;code&gt;Function Scan&lt;/code&gt; is a leaf. Read their &lt;code&gt;actual time × loops&lt;/code&gt; for total per-leaf cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move 2 — find the hot leaf.&lt;/strong&gt; The leaf with the largest &lt;code&gt;actual time × loops&lt;/code&gt; is the bottleneck. If &lt;code&gt;Seq Scan on orders&lt;/code&gt; is 1200 ms and everything else is 5 ms, the fix is at that scan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move 3 — check &lt;code&gt;Rows Removed by Filter&lt;/code&gt;.&lt;/strong&gt; If a &lt;code&gt;Seq Scan&lt;/code&gt; has &lt;code&gt;Filter: created_at &amp;gt; NOW() - INTERVAL '30 days'&lt;/code&gt; and &lt;code&gt;Rows Removed by Filter: 99000000&lt;/code&gt;, the filter should be an index. Add one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move 4 — check estimate-vs-actual.&lt;/strong&gt; For each node, compare &lt;code&gt;rows=&lt;/code&gt; to &lt;code&gt;actual rows=&lt;/code&gt;. A 10× gap is worth investigating; a 100× gap almost always means a bad plan. Run &lt;code&gt;ANALYZE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move 5 — check &lt;code&gt;BUFFERS: temp read&lt;/code&gt;.&lt;/strong&gt; Non-zero temp read = spill. Fix by rewriting the query or bumping &lt;code&gt;work_mem&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 5 — the estimate-vs-actual failure modes.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stale statistics.&lt;/strong&gt; After a data load, the histograms and MCV lists are out of date. Run &lt;code&gt;ANALYZE table&lt;/code&gt; to refresh.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skewed columns without extended stats.&lt;/strong&gt; A column with 90% of rows having value &lt;code&gt;'X'&lt;/code&gt; and 10% having other values will fool the optimiser unless you create extended statistics: &lt;code&gt;CREATE STATISTICS s ON col1, col2 FROM t;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correlated columns.&lt;/strong&gt; If &lt;code&gt;country&lt;/code&gt; and &lt;code&gt;city&lt;/code&gt; are highly correlated but the planner assumes independence, it can be off by 100× on multi-column predicates. Fix — extended statistics on &lt;code&gt;(country, city)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Function-wrapped columns.&lt;/strong&gt; &lt;code&gt;WHERE EXTRACT(YEAR FROM created_at) = 2026&lt;/code&gt; is not sargable — the planner can't use the histogram. Rewrite: &lt;code&gt;WHERE created_at &amp;gt;= '2026-01-01' AND created_at &amp;lt; '2027-01-01'&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prepared-statement plan reuse.&lt;/strong&gt; With &lt;code&gt;plan_cache_mode = 'auto'&lt;/code&gt;, Postgres may pick a generic plan that is bad for specific parameter values. Force &lt;code&gt;plan_cache_mode = 'force_custom_plan'&lt;/code&gt; if the shape hurts you.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 6 — the eight most common Postgres plan pathologies.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Seq Scan&lt;/code&gt; on a large filtered table.&lt;/strong&gt; Missing index. Fix — &lt;code&gt;CREATE INDEX ON t (filter_col);&lt;/code&gt; or a composite index if multiple filters combine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Nested Loop&lt;/code&gt; with millions on the outer side.&lt;/strong&gt; Very slow — should be &lt;code&gt;Hash Join&lt;/code&gt;. Fix — check estimates; if the planner underestimated the outer, run &lt;code&gt;ANALYZE&lt;/code&gt;. If the estimate is right and the plan is still bad, raise &lt;code&gt;enable_nestloop = off&lt;/code&gt; (dangerous — use only after diagnosis).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Sort&lt;/code&gt; with &lt;code&gt;external merge&lt;/code&gt; method.&lt;/strong&gt; Spilling. Fix — rewrite to sort less (add &lt;code&gt;LIMIT&lt;/code&gt;, smaller GROUP BY, pre-aggregate), or bump &lt;code&gt;work_mem&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Hash Join&lt;/code&gt; with &lt;code&gt;temp read&lt;/code&gt; non-zero.&lt;/strong&gt; Spilling. Fix — swap build side (planner may have picked the wrong side), or bump &lt;code&gt;work_mem&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Bitmap Heap Scan&lt;/code&gt; reading most of the table.&lt;/strong&gt; The bitmap is close to full — the index isn't selective enough. Fix — either accept &lt;code&gt;Seq Scan&lt;/code&gt; (which is what the planner would pick with better stats), or add a partial index.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Index Scan&lt;/code&gt; with high &lt;code&gt;Rows Removed by Index Recheck&lt;/code&gt;.&lt;/strong&gt; The index was used but many rows didn't match the full filter — the index only covered part of the predicate. Fix — extend the index to a covering &lt;code&gt;INCLUDE&lt;/code&gt; index.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Parallel Seq Scan&lt;/code&gt; with &lt;code&gt;Workers Launched: 0&lt;/code&gt;.&lt;/strong&gt; Parallelism was disabled — usually because &lt;code&gt;max_parallel_workers_per_gather = 0&lt;/code&gt; or the table is too small (&lt;code&gt;min_parallel_table_scan_size&lt;/code&gt;). Fix — configure appropriately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;CTE Scan&lt;/code&gt; on a materialised CTE.&lt;/strong&gt; Pre-PG12 CTEs were materialised (always). PG12+ inlines by default; if a CTE is materialised on new Postgres, it's an explicit &lt;code&gt;WITH ... AS MATERIALIZED&lt;/code&gt; — verify that was intentional.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 7 — &lt;code&gt;EXPLAIN&lt;/code&gt; variants you should know.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;EXPLAIN query&lt;/code&gt;&lt;/strong&gt; — plan-only, no execution. Free but based on estimates only. Useful for expensive queries or when you can't afford to run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;EXPLAIN ANALYZE query&lt;/code&gt;&lt;/strong&gt; — runs the query and returns actual timings. The everyday variant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;EXPLAIN (ANALYZE, BUFFERS)&lt;/code&gt;&lt;/strong&gt; — adds the &lt;code&gt;BUFFERS&lt;/code&gt; triangle. Use always for on-call diagnosis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;EXPLAIN (ANALYZE, BUFFERS, VERBOSE)&lt;/code&gt;&lt;/strong&gt; — adds column projections. Useful for projection issues.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)&lt;/code&gt;&lt;/strong&gt; — machine-readable. Great for CI regression tests, plan-diffing tooling, and &lt;code&gt;pg_stat_statements&lt;/code&gt; correlation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;EXPLAIN (ANALYZE, BUFFERS, WAL)&lt;/code&gt;&lt;/strong&gt; — adds WAL (write-ahead log) accounting for DML. Useful when diagnosing slow INSERTs / UPDATEs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;EXPLAIN (ANALYZE, BUFFERS, SETTINGS)&lt;/code&gt;&lt;/strong&gt; — adds non-default GUC settings. Useful when a query works differently on prod vs a replica.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;Common beginner mistakes&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Running &lt;code&gt;EXPLAIN&lt;/code&gt; without &lt;code&gt;ANALYZE&lt;/code&gt; on a slow query and drawing conclusions from estimates only.&lt;/li&gt;
&lt;li&gt;Bumping &lt;code&gt;work_mem&lt;/code&gt; globally after seeing a single spill — every backend gets its own copy and OOM under concurrency.&lt;/li&gt;
&lt;li&gt;Adding an index without checking that the plan changes — sometimes the planner still prefers &lt;code&gt;Seq Scan&lt;/code&gt; due to bad stats or low selectivity.&lt;/li&gt;
&lt;li&gt;Reading top-down — missing which leaf is the actual bottleneck.&lt;/li&gt;
&lt;li&gt;Ignoring &lt;code&gt;loops&lt;/code&gt; — a &lt;code&gt;Nested Loop&lt;/code&gt; with &lt;code&gt;actual time=0.1ms loops=1000000&lt;/code&gt; costs 100 seconds, not 0.1 ms.&lt;/li&gt;
&lt;li&gt;Copying &lt;code&gt;EXPLAIN&lt;/code&gt; output into Slack without formatting — a plan tree is nearly unreadable when line breaks are lost.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — the &lt;code&gt;Seq Scan&lt;/code&gt; that should have been an &lt;code&gt;Index Scan&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The most common Postgres plan bug: a &lt;code&gt;Seq Scan&lt;/code&gt; on a large table where a filter has good selectivity. The plan shows a &lt;code&gt;Seq Scan&lt;/code&gt; with &lt;code&gt;Rows Removed by Filter&lt;/code&gt; in the millions; the fix is a single-column index.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given the plan below on an &lt;code&gt;orders&lt;/code&gt; table (100 M rows), identify the fix. Show the DDL that would flip the plan to &lt;code&gt;Index Scan&lt;/code&gt; (or &lt;code&gt;Bitmap Heap Scan&lt;/code&gt;) and reduce wall clock from ~1200 ms to under 20 ms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;QUERY PLAN
Limit  (cost=... rows=100 width=16) (actual time=1200.5..1200.6 rows=100 loops=1)
  Buffers: shared hit=1234 read=456
  -&amp;gt;  Sort  (cost=... rows=100 width=16) (actual time=1200.4..1200.5 rows=100 loops=1)
        -&amp;gt;  Seq Scan on orders o  (cost=... rows=1000000 width=12) (actual time=99.0..1150.0 rows=1000000 loops=1)
              Filter: (created_at &amp;gt;= now() - '30 days'::interval)
              Rows Removed by Filter: 99000000
              Buffers: shared hit=1234 read=456
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Fix: add index on the filter column&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;CONCURRENTLY&lt;/span&gt; &lt;span class="n"&gt;idx_orders_created_at&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Verify the plan flip&lt;/span&gt;
&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUFFERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'30 days'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The current plan reads 100 M rows (&lt;code&gt;Seq Scan on orders&lt;/code&gt;) and discards 99 M via the &lt;code&gt;created_at&lt;/code&gt; filter — clear signal of a missing index on &lt;code&gt;created_at&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CREATE INDEX CONCURRENTLY&lt;/code&gt; — the &lt;code&gt;CONCURRENTLY&lt;/code&gt; clause avoids taking an &lt;code&gt;ACCESS EXCLUSIVE&lt;/code&gt; lock, so writes continue during index build. Takes longer (double-scan) but keeps prod live.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DESC&lt;/code&gt; in the index matches the query's &lt;code&gt;ORDER BY SUM(total) DESC&lt;/code&gt; — but the sort is on the aggregate, not &lt;code&gt;created_at&lt;/code&gt;; so &lt;code&gt;DESC&lt;/code&gt; here doesn't help ordering. It's still fine because btree indexes can be scanned in either direction.&lt;/li&gt;
&lt;li&gt;After the index exists, run &lt;code&gt;ANALYZE orders&lt;/code&gt; (or wait for autovacuum). The optimiser now has fresh selectivity estimates for &lt;code&gt;created_at&lt;/code&gt; and will pick &lt;code&gt;Index Scan&lt;/code&gt; or &lt;code&gt;Bitmap Heap Scan&lt;/code&gt; when the filter is selective enough.&lt;/li&gt;
&lt;li&gt;Re-run &lt;code&gt;EXPLAIN (ANALYZE, BUFFERS)&lt;/code&gt; — the new plan should show &lt;code&gt;Bitmap Heap Scan&lt;/code&gt; or &lt;code&gt;Index Scan&lt;/code&gt; on &lt;code&gt;orders&lt;/code&gt; with &lt;code&gt;actual rows=1000000&lt;/code&gt; (the filter-matching rows only) and wall clock under 20 ms.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Seq Scan on orders&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Bitmap Heap Scan on orders&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;actual rows=1000000 loops=1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;actual rows=1000000 loops=1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Rows Removed by Filter: 99000000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Rows Removed by Filter: 0&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Buffers: shared read=456&lt;/td&gt;
&lt;td&gt;Buffers: shared read=12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wall clock ~1200 ms&lt;/td&gt;
&lt;td&gt;Wall clock ~15 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Any &lt;code&gt;Seq Scan&lt;/code&gt; with &lt;code&gt;Rows Removed by Filter&lt;/code&gt; in the millions is a missing-index diagnosis. The DDL is one line; the wall-clock win is 80–100× typical.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the &lt;code&gt;Sort&lt;/code&gt; that spills and how &lt;code&gt;work_mem&lt;/code&gt; fixes it
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A common failure mode: a &lt;code&gt;Sort&lt;/code&gt; node with &lt;code&gt;Sort Method: external merge&lt;/code&gt; and &lt;code&gt;temp read&lt;/code&gt; in the buffers. The engine ran out of &lt;code&gt;work_mem&lt;/code&gt; and spilled to a temp file. Two fixes — rewrite to sort less, or increase &lt;code&gt;work_mem&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given the plan below with a spilled &lt;code&gt;Sort&lt;/code&gt;, show two fixes and their trade-offs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sort  (cost=... rows=10000000 width=64) (actual time=8500.0..12000.0 rows=10000000 loops=1)
  Sort Key: total DESC
  Sort Method: external merge  Disk: 640000kB
  Buffers: shared hit=5000, temp read=80000 written=80000
  -&amp;gt;  Seq Scan on orders  (cost=... rows=10000000 width=64) (actual time=0.5..2200.0 rows=10000000 loops=1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Fix A — bump work_mem for this session only (safe, cheap)&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;work_mem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'256MB'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;RESET&lt;/span&gt; &lt;span class="n"&gt;work_mem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Fix B — rewrite to sort less (best when you only want top-N)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Postgres picks Sort Method: top-N heapsort — bounded memory, no spill&lt;/span&gt;

&lt;span class="c1"&gt;-- Fix C — pre-aggregate before sorting (best when downstream only needs summaries)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The current plan sorts 10 M rows and spills 640 MB to disk (&lt;code&gt;Sort Method: external merge Disk: 640000kB&lt;/code&gt;). Wall clock is 12 s.&lt;/li&gt;
&lt;li&gt;Fix A — &lt;code&gt;SET work_mem = '256MB'&lt;/code&gt; for the session. The &lt;code&gt;Sort&lt;/code&gt; fits in memory, method changes to &lt;code&gt;quicksort&lt;/code&gt;, wall clock drops to ~3 s. Never &lt;code&gt;ALTER SYSTEM SET work_mem = '256MB'&lt;/code&gt; — under concurrency, 100 sessions × 256 MB = 25 GB of hash / sort memory.&lt;/li&gt;
&lt;li&gt;Fix B — add &lt;code&gt;LIMIT 100&lt;/code&gt;. The planner picks &lt;code&gt;Sort Method: top-N heapsort&lt;/code&gt; — a bounded heap of size N. Memory usage is &lt;code&gt;N × row_width&lt;/code&gt;, not full-table. Wall clock drops to ~2 s.&lt;/li&gt;
&lt;li&gt;Fix C — pre-aggregate. Instead of sorting 10 M raw rows, sort 100 K aggregated rows. &lt;code&gt;Hash Aggregate → Sort → Limit&lt;/code&gt;. Wall clock ~500 ms.&lt;/li&gt;
&lt;li&gt;Prefer C &amp;gt; B &amp;gt; A. Rewriting the query is usually cheaper than tuning memory; increasing memory is the last resort when the query truly needs a big sort.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;th&gt;Sort Method&lt;/th&gt;
&lt;th&gt;Disk spill&lt;/th&gt;
&lt;th&gt;Wall clock&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;external merge&lt;/td&gt;
&lt;td&gt;640 MB&lt;/td&gt;
&lt;td&gt;12 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A: &lt;code&gt;work_mem=256MB&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;quicksort&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;3 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B: &lt;code&gt;LIMIT 100&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;top-N heapsort&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;2 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C: pre-aggregate + LIMIT&lt;/td&gt;
&lt;td&gt;quicksort&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;500 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Diagnose spill first — the &lt;code&gt;Sort Method: external merge&lt;/code&gt; line and non-zero &lt;code&gt;temp read&lt;/code&gt; are the smoking gun. Prefer to rewrite the query; bump &lt;code&gt;work_mem&lt;/code&gt; only for the session, never globally.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — &lt;code&gt;EXPLAIN (ANALYZE, BUFFERS)&lt;/code&gt; on a &lt;code&gt;Hash Join&lt;/code&gt; bottleneck
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A &lt;code&gt;Hash Join&lt;/code&gt; between two large tables can dominate wall clock when the build side is bigger than expected. Reading &lt;code&gt;BUFFERS&lt;/code&gt; reveals whether the hash is spilling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given the plan below with a slow &lt;code&gt;Hash Join&lt;/code&gt;, walk through diagnosis. Which side is the build side? What does &lt;code&gt;temp read&lt;/code&gt; on the hash imply? What's the fix?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hash Join  (cost=... rows=1000000 width=32) (actual time=4500.0..8500.0 rows=980000 loops=1)
  Hash Cond: (o.customer_id = c.id)
  Buffers: shared hit=1000 read=4500 temp read=25000 written=25000
  -&amp;gt;  Seq Scan on orders o  (cost=... rows=10000000 width=16) (actual time=0.5..3000.0 rows=10000000 loops=1)
  -&amp;gt;  Hash  (cost=... rows=5000000 width=20) (actual time=4200.0..4200.0 rows=5000000 loops=1)
        Buckets: 65536 Batches: 8 Memory Usage: 4096kB
        -&amp;gt;  Seq Scan on customers c  (cost=... rows=5000000 width=20) (actual time=0.5..2000.0 rows=5000000 loops=1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Fix A — bump work_mem for the session&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;work_mem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'256MB'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Fix B — swap the build side by rewriting (planner picked wrong)&lt;/span&gt;
&lt;span class="c1"&gt;-- The customers table is bigger than orders in this query, but customers should be filtered first&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'US'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Now the hash is built on filtered customers (much smaller), not raw customers&lt;/span&gt;

&lt;span class="c1"&gt;-- Fix C — force the planner via a subquery / CTE materialisation&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;us_customers&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'US'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;us_customers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;Hash&lt;/code&gt; is the build side — Postgres always builds the hash on the &lt;em&gt;bottom child&lt;/em&gt; of &lt;code&gt;Hash Join&lt;/code&gt;. Here, that's &lt;code&gt;customers c&lt;/code&gt; (5 M rows).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Batches: 8&lt;/code&gt; — the hash was partitioned into 8 batches because it didn't fit in &lt;code&gt;work_mem&lt;/code&gt;. Batches &amp;gt; 1 means spilling.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Buffers: shared hit=1000 read=4500 temp read=25000&lt;/code&gt; — the &lt;code&gt;temp read=25000&lt;/code&gt; is the smoking gun. 25 000 pages × 8 KB = 200 MB spilled.&lt;/li&gt;
&lt;li&gt;Fix A — bump &lt;code&gt;work_mem&lt;/code&gt;. If bumped to 256 MB, &lt;code&gt;Batches&lt;/code&gt; drops to 1 and &lt;code&gt;temp read&lt;/code&gt; goes to 0; wall clock roughly halves.&lt;/li&gt;
&lt;li&gt;Fix B — swap sides. If &lt;code&gt;customers&lt;/code&gt; is filtered by a selectivity predicate (&lt;code&gt;country = 'US'&lt;/code&gt;), do the filter &lt;em&gt;before&lt;/em&gt; the join. The filtered customers hash is 500 K rows (10% of 5 M), fits in &lt;code&gt;work_mem&lt;/code&gt;, no spill.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;th&gt;Batches&lt;/th&gt;
&lt;th&gt;temp read&lt;/th&gt;
&lt;th&gt;Wall clock&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;200 MB&lt;/td&gt;
&lt;td&gt;8.5 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A: work_mem=256MB&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;4.5 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B: pre-filter build side&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1.5 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C: MATERIALIZED CTE&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1.5 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; &lt;code&gt;Batches &amp;gt; 1&lt;/code&gt; on a &lt;code&gt;Hash&lt;/code&gt; node = spilling. Prefer to filter the build side down before the join; &lt;code&gt;work_mem&lt;/code&gt; bump is the last resort.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;postgres explain analyze&lt;/code&gt; interview question on a two-plan diff
&lt;/h3&gt;

&lt;p&gt;A senior interviewer often asks: "Here are two &lt;code&gt;EXPLAIN (ANALYZE, BUFFERS)&lt;/code&gt; outputs for the same query — one from prod and one from a staging DB with the same data. Prod is 30 seconds; staging is 300 ms. Walk me through the diff and name the root cause."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using bottom-up plan diffing to isolate the regressing node
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- Prod plan (slow — 30 s)
Limit  (actual time=30000..30000 rows=100 loops=1)
  Buffers: shared hit=5000 read=45000 temp read=8000
  -&amp;gt;  Sort  (actual time=29995..30000 rows=100 loops=1)
        Sort Method: external merge  Disk: 64000kB
        -&amp;gt;  HashAggregate  (actual time=28000..29500 rows=850000 loops=1)
              -&amp;gt;  Hash Join  (actual time=100..27000 rows=1000000 loops=1)
                    Buffers: temp read=8000
                    -&amp;gt;  Seq Scan on orders  (actual time=0.5..12000 rows=100000000 loops=1)
                    -&amp;gt;  Hash  (actual time=500..500 rows=5000000 loops=1)
                          Batches: 8  Memory Usage: 4096kB
                          -&amp;gt;  Seq Scan on customers  (actual time=0.5..300 rows=5000000 loops=1)

-- Staging plan (fast — 300 ms)
Limit  (actual time=290..300 rows=100 loops=1)
  Buffers: shared hit=1500 read=200
  -&amp;gt;  Sort  (actual time=289..300 rows=100 loops=1)
        Sort Method: top-N heapsort  Memory: 33kB
        -&amp;gt;  HashAggregate  (actual time=250..280 rows=850000 loops=1)
              -&amp;gt;  Hash Join  (actual time=50..220 rows=1000000 loops=1)
                    -&amp;gt;  Bitmap Heap Scan on orders  (actual time=1..80 rows=1000000 loops=1)
                          Recheck Cond: (created_at &amp;gt;= '2026-06-10')
                          -&amp;gt;  Bitmap Index Scan on idx_orders_created_at
                    -&amp;gt;  Hash  (actual time=100..100 rows=5000000 loops=1)
                          Batches: 1  Memory Usage: 512000kB
                          -&amp;gt;  Seq Scan on customers  (actual time=0.5..80 rows=5000000 loops=1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Node&lt;/th&gt;
&lt;th&gt;Prod actual time&lt;/th&gt;
&lt;th&gt;Staging actual time&lt;/th&gt;
&lt;th&gt;Diff&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Seq/Bitmap Scan on orders&lt;/td&gt;
&lt;td&gt;12000 ms (Seq, 100M rows)&lt;/td&gt;
&lt;td&gt;80 ms (Bitmap, 1M rows)&lt;/td&gt;
&lt;td&gt;Missing index on prod&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hash (customers)&lt;/td&gt;
&lt;td&gt;500 ms, Batches: 8&lt;/td&gt;
&lt;td&gt;100 ms, Batches: 1&lt;/td&gt;
&lt;td&gt;work_mem too low on prod&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sort&lt;/td&gt;
&lt;td&gt;external merge, 64 MB spill&lt;/td&gt;
&lt;td&gt;top-N heapsort, 33 kB&lt;/td&gt;
&lt;td&gt;Sort method differs — cascades from filter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total wall clock&lt;/td&gt;
&lt;td&gt;30 s&lt;/td&gt;
&lt;td&gt;300 ms&lt;/td&gt;
&lt;td&gt;100× gap&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Prod is missing an index on &lt;code&gt;orders(created_at)&lt;/code&gt; — the &lt;code&gt;Seq Scan&lt;/code&gt; reads 100 M rows and the filter discards 99 M. Staging has the index; the plan flips to &lt;code&gt;Bitmap Heap Scan&lt;/code&gt; reading only 1 M matched rows. Prod's &lt;code&gt;work_mem&lt;/code&gt; is also too low — the customers hash spills to 8 batches; staging has bigger &lt;code&gt;work_mem&lt;/code&gt; and fits in 1 batch. Two fixes, one root cause per — but the dominant fix is the index.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Fix priority&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;th&gt;Est. wall clock after&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CREATE INDEX CONCURRENTLY idx_orders_created_at ON orders(created_at);&lt;/code&gt; then &lt;code&gt;ANALYZE orders;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;~500 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ALTER SYSTEM SET work_mem = '32MB';&lt;/code&gt; (was 4 MB) then restart / reload&lt;/td&gt;
&lt;td&gt;~350 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Ensure both plans are on Postgres 15+ (top-N heapsort improvements)&lt;/td&gt;
&lt;td&gt;~300 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Bottom-up diff&lt;/strong&gt;&lt;/strong&gt; — comparing plan trees leaf-first surfaces the storage-access divergence immediately. The prod &lt;code&gt;Seq Scan&lt;/code&gt; vs staging &lt;code&gt;Bitmap Heap Scan&lt;/code&gt; is a one-line diff that pinpoints the missing index.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;&lt;code&gt;Rows Removed by Filter&lt;/code&gt;&lt;/strong&gt;&lt;/strong&gt; — even without staging as a reference, prod's plan alone shows 99 M rows discarded by a filter. That is the missing-index tell every time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;&lt;code&gt;Batches: N &amp;gt; 1&lt;/code&gt;&lt;/strong&gt;&lt;/strong&gt; — the &lt;code&gt;Hash&lt;/code&gt; node's &lt;code&gt;Batches: 8&lt;/code&gt; line means the hash spilled to 8 partitions on disk. Combined with &lt;code&gt;Memory Usage: 4096kB&lt;/code&gt; and &lt;code&gt;Buffers: temp read=8000&lt;/code&gt;, it names the spill and its size (~64 MB).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;&lt;code&gt;Sort Method&lt;/code&gt; divergence&lt;/strong&gt;&lt;/strong&gt; — prod uses &lt;code&gt;external merge&lt;/code&gt; (spilled); staging uses &lt;code&gt;top-N heapsort&lt;/code&gt; (bounded). The method differs because the row count reaching the sort differs — the filter pathology upstream cascades all the way down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — Diagnosis cost = one &lt;code&gt;EXPLAIN (ANALYZE, BUFFERS)&lt;/code&gt; × 2 plans = 30 s + 300 ms wall clock. Fix cost = one &lt;code&gt;CREATE INDEX CONCURRENTLY&lt;/code&gt; (typically minutes on 100 M rows, non-blocking) + &lt;code&gt;ANALYZE&lt;/code&gt;. Downstream cost = zero — the index also benefits every other &lt;code&gt;orders(created_at)&lt;/code&gt; filter in the codebase.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — optimization&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL optimization drills&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/optimization" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — indexing&lt;/span&gt;
&lt;strong&gt;Composite index and covering-index drills&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/indexing" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  3. Snowflake QUERY_PROFILE
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;snowflake query profile&lt;/code&gt; — the operator stack, micro-partition pruning percent, and how local vs remote disk spill drives warehouse sizing
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;Snowflake's Query Profile presents the executed query as a stack of operator cards — TableScan, Filter, Aggregate, Join, Sort, WindowFunction — each annotated with bytes scanned, partitions total vs partitions scanned (pruning percent), row counts, and a spillage panel with local disk spill and remote disk spill in bytes; reading it top-down and hunting for low pruning percent plus non-zero remote spill is the entire diagnosis discipline&lt;/strong&gt;. Once you can navigate the Snowsight Query Profile tab and its underlying &lt;code&gt;QUERY_HISTORY&lt;/code&gt; view, you can size warehouses, spot bad table clustering, and rewrite joins in one pass.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foywzxs4p42kqrcruece5.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foywzxs4p42kqrcruece5.jpeg" alt="Visual diagram of the Snowflake Query Profile — left a warehouse and query-history strip showing where the profile lives, centre a stack of operator cards (TableScan, Filter, Aggregate, Join, Sort) each annotated with bytes scanned and partition pruning, right a spillage panel showing local vs remote disk spill bytes with a warehouse-sizing recommendation; on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Slot 1 — where to find the profile.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Snowsight → Activity → Query History → click into a query → Profile tab.&lt;/strong&gt; The graphical, interactive view. Every operator is a clickable card with bytes scanned, rows produced, and % of total execution time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SELECT * FROM SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY WHERE query_id = 'abc123';&lt;/code&gt;&lt;/strong&gt; — the underlying table. Available roughly 45 minutes to 3 hours delayed. Columns include &lt;code&gt;bytes_scanned&lt;/code&gt;, &lt;code&gt;partitions_scanned&lt;/code&gt;, &lt;code&gt;partitions_total&lt;/code&gt;, &lt;code&gt;bytes_spilled_to_local_storage&lt;/code&gt;, &lt;code&gt;bytes_spilled_to_remote_storage&lt;/code&gt;, &lt;code&gt;warehouse_size&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SELECT * FROM SNOWFLAKE.INFORMATION_SCHEMA.QUERY_HISTORY WHERE query_id = 'abc123';&lt;/code&gt;&lt;/strong&gt; — near-real-time (7 days retention). Same columns as ACCOUNT_USAGE but faster availability, shorter retention.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;GET_QUERY_OPERATOR_STATS(query_id)&lt;/code&gt;&lt;/strong&gt; — table function returning per-operator statistics. Machine-readable; use for programmatic profile analysis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;SYSTEM$EXPLAIN_PLAN_JSON(query_text)&lt;/code&gt; function&lt;/strong&gt; — returns the planned (pre-execution) plan as JSON. Useful when you can't afford to execute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;WEB_UI&lt;/code&gt; shortcut&lt;/strong&gt; — every query in Snowsight has a "Query Profile" link that opens the interactive view directly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 2 — the operator card anatomy.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TableScan&lt;/strong&gt; — the leaf. Reads from micro-partitions. Card shows &lt;code&gt;Bytes scanned&lt;/code&gt;, &lt;code&gt;Partitions total&lt;/code&gt;, &lt;code&gt;Partitions scanned&lt;/code&gt;, and &lt;code&gt;Pruning %&lt;/code&gt;. Pruning percent is the single most important number in every profile.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filter&lt;/strong&gt; — applies a WHERE predicate. Card shows input row count and output row count. The pushdown-into-scan is invisible on Snowflake — good filters are absorbed into the TableScan card.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JoinFilter&lt;/strong&gt; — a Bloom filter used to skip micro-partitions on the probe side of a hash join. Presence means Snowflake is doing dynamic partition pruning — a huge win when it applies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Join&lt;/strong&gt; — hash join (default), sort-merge join (rare), broadcast join (for small side). Card shows &lt;code&gt;Build side rows&lt;/code&gt;, &lt;code&gt;Probe side rows&lt;/code&gt;, and the join algorithm.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aggregate&lt;/strong&gt; — hash-based &lt;code&gt;GROUP BY&lt;/code&gt;. Card shows &lt;code&gt;input rows&lt;/code&gt;, &lt;code&gt;groups produced&lt;/code&gt;, &lt;code&gt;bytes spilled&lt;/code&gt; (if any).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sort&lt;/strong&gt; — order-by operator. Card shows &lt;code&gt;input rows&lt;/code&gt;, &lt;code&gt;bytes spilled&lt;/code&gt;, &lt;code&gt;sort keys&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WindowFunction&lt;/strong&gt; — analytic function (&lt;code&gt;ROW_NUMBER&lt;/code&gt;, &lt;code&gt;RANK&lt;/code&gt;, &lt;code&gt;SUM() OVER (...)&lt;/code&gt;). Card shows &lt;code&gt;partitions&lt;/code&gt;, &lt;code&gt;bytes spilled&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Result&lt;/strong&gt; — the final projection. Card shows &lt;code&gt;total rows returned&lt;/code&gt;, &lt;code&gt;total bytes returned to client&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Broadcast / DistributeByHash&lt;/strong&gt; — data-distribution nodes across the warehouse's cluster nodes. Presence of Broadcast usually means the planner detected a small side.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execution time %&lt;/strong&gt; — every card has a per-operator percent of total execution time. Sort operators by this — the hot operator is the fix target.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 3 — micro-partition pruning percent (the single most important number).&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Partitions total&lt;/code&gt;&lt;/strong&gt; — how many micro-partitions the table has in total. Micro-partitions are ~50–500 MB chunks Snowflake maintains internally; you don't create or manage them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Partitions scanned&lt;/code&gt;&lt;/strong&gt; — how many were actually read. Ideally a tiny fraction of total.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Pruning % = 1 - partitions_scanned / partitions_total&lt;/code&gt;&lt;/strong&gt; — the % of micro-partitions skipped by min/max pruning on the WHERE clause. &lt;strong&gt;99%+ is great. 50% means half your storage cost. 0% means the filter was not sargable or the table wasn't clustered on the filter column.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How Snowflake prunes.&lt;/strong&gt; For each micro-partition, it stores min and max of each column. A &lt;code&gt;WHERE created_at &amp;gt;= '2026-06-10'&lt;/code&gt; predicate can eliminate any micro-partition whose max &amp;lt; '2026-06-10'. Pruning happens before scan — no bytes read from pruned partitions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When pruning fails.&lt;/strong&gt; Function-wrapped columns (&lt;code&gt;DATE(created_at)&lt;/code&gt;), non-clustered columns, complex predicates the optimiser can't push down. Fix — rewrite the predicate to be sargable, or &lt;code&gt;CLUSTER BY&lt;/code&gt; the table on the filter column.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 4 — local vs remote disk spill and warehouse sizing.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Local disk spill = spill onto the warehouse node's SSD.&lt;/strong&gt; Slower than RAM but manageable. &lt;code&gt;bytes_spilled_to_local_storage&lt;/code&gt; non-zero means the warehouse ran out of memory but is still within its SSD budget.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remote disk spill = spill onto Snowflake's remote storage (S3-backed).&lt;/strong&gt; Slow — 10–100× slower than local spill. &lt;code&gt;bytes_spilled_to_remote_storage&lt;/code&gt; non-zero is the strongest signal that the warehouse is too small.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Warehouse sizing rule of thumb.&lt;/strong&gt; If a query has non-zero remote spill, escalate one warehouse tier (X-Small → Small → Medium → Large → X-Large → 2XL → 3XL → 4XL). Each tier is roughly 2× the compute and 2× the memory. Remote spill going to zero usually happens within one or two tier bumps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost trade-off.&lt;/strong&gt; Each tier bump doubles cost per minute. If a query is remote-spilling and you bump L → XL, you pay 2× per minute but the query may finish in 1/3 the time — net savings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-suspend and multi-cluster.&lt;/strong&gt; Auto-suspend is orthogonal; it kills the warehouse after idle. Multi-cluster is for concurrency (multiple queries at once), not for individual query size.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 5 — bytes scanned vs partitions scanned.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;bytes_scanned&lt;/code&gt;&lt;/strong&gt; — the compressed bytes Snowflake read from storage. Columnar reads mean only projected columns are counted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;partitions_scanned&lt;/code&gt;&lt;/strong&gt; — a proxy for I/O. Each micro-partition is ~50–500 MB uncompressed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;bytes_written_to_result&lt;/code&gt; / &lt;code&gt;bytes_written&lt;/code&gt;&lt;/strong&gt; — outbound data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost model on Snowflake&lt;/strong&gt; — compute is time-billed (per second on the warehouse tier), not bytes-billed. So &lt;code&gt;bytes_scanned&lt;/code&gt; is a diagnostic, not a bill line. The bill is warehouse tier × query wall clock.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimising bytes scanned&lt;/strong&gt; — pick fewer columns (&lt;code&gt;SELECT a, b, c&lt;/code&gt; not &lt;code&gt;SELECT *&lt;/code&gt;), push predicates to prune, use &lt;code&gt;CLUSTER BY&lt;/code&gt; on high-filter columns.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 6 — reading a Snowflake profile top-down.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Move 1 — sort operators by execution time %.&lt;/strong&gt; The hot operator is the one with 40%+ of total time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move 2 — check the hot operator's card.&lt;/strong&gt; If it's a TableScan, check pruning %. If it's a Join, check build vs probe sizes. If it's an Aggregate, check spillage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move 3 — check spillage panel.&lt;/strong&gt; Non-zero remote spill = warehouse too small. Non-zero local spill = borderline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move 4 — check bytes scanned across TableScans.&lt;/strong&gt; If one TableScan reads 90% of total bytes, is it necessary? Can columns be projected away or partitions pruned?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move 5 — name the fix.&lt;/strong&gt; Cluster the table. Rewrite the filter. Bump warehouse. Add a materialised view. Rewrite the join.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 7 — the eight most common Snowflake profile pathologies.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Pruning % = 0&lt;/code&gt;.&lt;/strong&gt; Full-table scan. Either the filter isn't sargable or the table isn't clustered on the filter column. Fix — rewrite the filter (&lt;code&gt;DATE(created_at) = '2026-07-01'&lt;/code&gt; → &lt;code&gt;created_at BETWEEN '2026-07-01' AND '2026-07-02'&lt;/code&gt;) or add &lt;code&gt;CLUSTER BY (created_at)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-zero remote disk spill.&lt;/strong&gt; Warehouse too small. Bump one tier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JoinFilter not present.&lt;/strong&gt; Snowflake didn't detect an opportunity for Bloom-filter dynamic pruning. Usually because one side is huge and the other is not filtered. Rewrite the query to filter the build side first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Broadcast join with a large "small" side.&lt;/strong&gt; The planner broadcast a table that isn't actually small. Verify with &lt;code&gt;SELECT COUNT(*) FROM small_side&lt;/code&gt; — if it's over ~1M rows, force a shuffle join by hinting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cartesian join (accidental cross join).&lt;/strong&gt; Card shows 10× the expected row count. Root cause is a missing join key. Search the query text for a missing &lt;code&gt;ON&lt;/code&gt; clause.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sort with high spill.&lt;/strong&gt; Same as Postgres — either rewrite to sort less (add &lt;code&gt;LIMIT&lt;/code&gt;, pre-aggregate), or bump warehouse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Window function on a huge partition.&lt;/strong&gt; &lt;code&gt;ROW_NUMBER() OVER (PARTITION BY user_id)&lt;/code&gt; on a table where one user has 100 M rows spills catastrophically. Fix — pre-filter to relevant users, or handle skew explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QUERY_HISTORY delay hurts on-call.&lt;/strong&gt; ACCOUNT_USAGE.QUERY_HISTORY can be up to 3 hours delayed. For real-time on-call, use INFORMATION_SCHEMA.QUERY_HISTORY (near-real-time, 7 days).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 8 — interview probes on Snowflake plan reading.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"How do you read a Snowflake query profile?"&lt;/strong&gt; — Open Snowsight Query History, click into the query, open the Profile tab. Sort operators by execution time %. Check hot operator's card (pruning %, spillage, join sides). Name fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"What is micro-partition pruning?"&lt;/strong&gt; — Snowflake maintains min/max per column per micro-partition. &lt;code&gt;WHERE col &amp;lt; X&lt;/code&gt; eliminates partitions with min &amp;gt; X. Pruning % tells you the effectiveness of the filter against the physical layout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Why is my query slow at page 1M on OFFSET pagination in Snowflake?"&lt;/strong&gt; — Micro-partition pruning helps for the initial WHERE, but OFFSET's scan-and-discard is amplified by columnar reads. Switch to keyset (previous blog).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"When would you use &lt;code&gt;CLUSTER BY&lt;/code&gt;?"&lt;/strong&gt; — When a table is very large (multiple TB), has a well-defined filter column (usually date), and pruning % is low. Cost — Snowflake maintains clustering in the background; cost per TB per month is documented.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"How do you size a warehouse?"&lt;/strong&gt; — Start Medium. Watch for remote spill. If zero, drop to Small; if non-zero, bump to Large. Iterate. Multi-cluster is orthogonal (for concurrency).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;Common beginner mistakes&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reading the profile bottom-up (Postgres reflex) — Snowflake profiles read top-down.&lt;/li&gt;
&lt;li&gt;Ignoring pruning % — a full-table scan at 0% pruning is silent unless you look at that number.&lt;/li&gt;
&lt;li&gt;Confusing local and remote spill — remote is much more expensive.&lt;/li&gt;
&lt;li&gt;Only bumping warehouse without checking pruning — often the fix is to rewrite the filter, not scale up.&lt;/li&gt;
&lt;li&gt;Missing that &lt;code&gt;SELECT *&lt;/code&gt; costs more than &lt;code&gt;SELECT a, b, c&lt;/code&gt; — Snowflake is columnar; fewer projected columns = fewer bytes scanned.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — the TableScan with 0% pruning that a &lt;code&gt;CLUSTER BY&lt;/code&gt; cures
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The archetype: a fact table sliced by &lt;code&gt;date_col&lt;/code&gt; with a &lt;code&gt;WHERE date_col = X&lt;/code&gt; filter shows pruning % of 0. The table isn't clustered on &lt;code&gt;date_col&lt;/code&gt;; the min/max of each micro-partition covers the whole date range, so no partition can be pruned. The fix is &lt;code&gt;CLUSTER BY (date_col)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given the profile card below on a &lt;code&gt;events&lt;/code&gt; table (200 TB, 800 M micro-partitions) with &lt;code&gt;WHERE event_date = '2026-07-10'&lt;/code&gt;, name the fix. Show the DDL and the expected pruning % after.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TableScan (events)
  Partitions total:     800,000
  Partitions scanned:   800,000
  Pruning %:            0.0%
  Bytes scanned:        180 GB
  Rows produced:        4,200 M
  Execution time %:     78%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Fix: cluster the table by event_date&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;CLUSTER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_date&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Snowflake reclusters in the background; monitor with:&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;SYSTEM&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;CLUSTERING_INFORMATION&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'(event_date)'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- After clustering completes (typically hours to days on 200 TB):&lt;/span&gt;
&lt;span class="c1"&gt;-- The same WHERE event_date = '2026-07-10' should show:&lt;/span&gt;
&lt;span class="c1"&gt;-- Pruning % ≈ 99.5%&lt;/span&gt;
&lt;span class="c1"&gt;-- Bytes scanned ≈ 900 MB&lt;/span&gt;
&lt;span class="c1"&gt;-- Wall clock ≈ 10× faster&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Current state — 800 K partitions, 800 K scanned = 0% pruning. Each micro-partition's &lt;code&gt;event_date&lt;/code&gt; min/max range covers so many days that no partition can be eliminated by the filter.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ALTER TABLE ... CLUSTER BY&lt;/code&gt; starts a background reclustering job. Snowflake rewrites micro-partitions so &lt;code&gt;event_date&lt;/code&gt; values are co-located. Cost — measured in credits per TB; documented in Snowflake pricing.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SYSTEM$CLUSTERING_INFORMATION&lt;/code&gt; returns a JSON describing the current clustering depth. &lt;code&gt;average_depth&lt;/code&gt; close to 1 means fully clustered.&lt;/li&gt;
&lt;li&gt;After clustering, &lt;code&gt;WHERE event_date = '2026-07-10'&lt;/code&gt; prunes down to the ~4 K micro-partitions covering that day — pruning % ≈ 99.5%.&lt;/li&gt;
&lt;li&gt;Rule of thumb — cluster on the column your dashboard filters on. Almost always a date or timestamp. Don't cluster on high-cardinality unrelated columns.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pruning % 0.0%&lt;/td&gt;
&lt;td&gt;Pruning % 99.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bytes scanned 180 GB&lt;/td&gt;
&lt;td&gt;Bytes scanned 900 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Partitions scanned 800,000&lt;/td&gt;
&lt;td&gt;Partitions scanned 4,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Execution time 78% of total&lt;/td&gt;
&lt;td&gt;Execution time 8% of total&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wall clock ~4 min&lt;/td&gt;
&lt;td&gt;Wall clock ~25 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Any TableScan with pruning % under 50% on a filter column is a clustering diagnosis. Cluster once, benefit every dashboard that filters on that column.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the remote spill that a warehouse bump cures
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A &lt;code&gt;Sort&lt;/code&gt; or &lt;code&gt;Aggregate&lt;/code&gt; operator with non-zero &lt;code&gt;bytes_spilled_to_remote_storage&lt;/code&gt; is the strongest single signal that the warehouse is undersized. One tier bump usually zeroes it out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given the profile card below on a &lt;code&gt;Sort&lt;/code&gt; operator with 220 GB remote spill on a Medium warehouse, name the fix. Show the ALTER WAREHOUSE that would cure it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sort
  Sort keys:                        event_date, user_id
  Input rows:                       4,200,000,000
  Output rows:                      4,200,000,000
  Bytes spilled to local storage:   80 GB
  Bytes spilled to remote storage:  220 GB
  Execution time %:                 61%
Warehouse:  MEDIUM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Fix A — bump the warehouse for this run&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="n"&gt;WAREHOUSE&lt;/span&gt; &lt;span class="n"&gt;analytics_wh&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;WAREHOUSE_SIZE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'LARGE'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Re-run query&lt;/span&gt;
&lt;span class="c1"&gt;-- Expected: local spill ~40 GB, remote spill 0&lt;/span&gt;

&lt;span class="c1"&gt;-- Fix B — rewrite the query to sort less (LIMIT, pre-aggregate)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;last_event&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_date&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_date&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Sort now operates on ~2M aggregated rows, not 4.2B raw rows&lt;/span&gt;

&lt;span class="c1"&gt;-- Fix C — if this is a one-off, use a larger warehouse only for this session&lt;/span&gt;
&lt;span class="n"&gt;USE&lt;/span&gt; &lt;span class="n"&gt;WAREHOUSE&lt;/span&gt; &lt;span class="n"&gt;big_wh&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;-- pre-created 2XL&lt;/span&gt;
&lt;span class="c1"&gt;-- run query&lt;/span&gt;
&lt;span class="n"&gt;USE&lt;/span&gt; &lt;span class="n"&gt;WAREHOUSE&lt;/span&gt; &lt;span class="n"&gt;analytics_wh&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Current state — Medium warehouse, 220 GB remote spill on the &lt;code&gt;Sort&lt;/code&gt;. Remote spill dominates wall clock; each byte spilled to remote storage is 10–100× slower than in-memory.&lt;/li&gt;
&lt;li&gt;Fix A — bump to Large. Large has 2× memory of Medium (~256 GB vs 128 GB for Sort spill headroom). Remote spill should drop to zero; local spill may remain but is manageable.&lt;/li&gt;
&lt;li&gt;Fix B — rewrite. If the query only needs top-N or pre-aggregated data, do that first. Sorting 4.2 B rows to keep 1000 is wasteful; pre-aggregate to 2 M rows, then sort.&lt;/li&gt;
&lt;li&gt;Fix C — separate warehouse. If this is a nightly job that hits Large only once, keep the daytime warehouse Medium and switch to a bigger warehouse only for the nightly step.&lt;/li&gt;
&lt;li&gt;Cost trade-off — Large is 2× Medium's per-second cost, but if wall clock drops from 30 min to 5 min, net cost drops.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;th&gt;Remote spill&lt;/th&gt;
&lt;th&gt;Local spill&lt;/th&gt;
&lt;th&gt;Wall clock&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;220 GB&lt;/td&gt;
&lt;td&gt;80 GB&lt;/td&gt;
&lt;td&gt;30 min&lt;/td&gt;
&lt;td&gt;30 × 1 = 30 units&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A: Large warehouse&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;40 GB&lt;/td&gt;
&lt;td&gt;8 min&lt;/td&gt;
&lt;td&gt;8 × 2 = 16 units&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B: pre-aggregate&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;3 min&lt;/td&gt;
&lt;td&gt;3 × 1 = 3 units&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C: dedicated 2XL for the job&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;4 min&lt;/td&gt;
&lt;td&gt;4 × 4 = 16 units&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Prefer B (rewrite) &amp;gt; A (bump warehouse) &amp;gt; C (dedicated warehouse). Rewriting is usually the cheapest and most durable fix.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the Broadcast join that broke on data growth
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Snowflake's planner picks broadcast join when one side is estimated small enough to distribute to all warehouse nodes. If the small side grows (say from 100 K rows to 20 M rows) but stats haven't refreshed, the broadcast may fail or become catastrophic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given the profile card below with a Broadcast join that's dominating wall clock, name the diagnosis and the fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Join (INNER)
  Type:           BROADCAST
  Build side:     dim_customer (20 M rows, 1.2 GB in memory)
  Probe side:     fact_orders (4.2 B rows)
  Execution time %: 55%
  Bytes spilled to remote: 8 GB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Diagnosis: dim_customer isn't small anymore; broadcasting 20M rows to every node is expensive.&lt;/span&gt;

&lt;span class="c1"&gt;-- Fix A — force a hash join instead of broadcast (use hint)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="cm"&gt;/*+ NO_BROADCAST(dim_customer) */&lt;/span&gt;
  &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;fact_orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;dim_customer&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2026-07-10'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Fix B — filter dim_customer down before the join&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;fact_orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;dim_customer&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;active_flag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2026-07-10'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Fix C — cluster fact_orders by event_date so the probe side prunes first&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;fact_orders&lt;/span&gt; &lt;span class="k"&gt;CLUSTER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_date&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Broadcast join built a hash on &lt;code&gt;dim_customer&lt;/code&gt; (20 M rows, 1.2 GB) and shipped it to every warehouse node. Memory pressure caused spill.&lt;/li&gt;
&lt;li&gt;Fix A — hint &lt;code&gt;NO_BROADCAST&lt;/code&gt; on &lt;code&gt;dim_customer&lt;/code&gt; forces a shuffle join; both sides are hash-partitioned by the join key. No broadcast means no per-node memory footprint.&lt;/li&gt;
&lt;li&gt;Fix B — filter the dim table to only the active customers (say 4 M rows). The broadcast becomes cheap again.&lt;/li&gt;
&lt;li&gt;Fix C — cluster the fact table so the &lt;code&gt;WHERE event_date&lt;/code&gt; prunes probe-side partitions first. Combined with (A) or (B), the query is near-optimal.&lt;/li&gt;
&lt;li&gt;Interview signal — knowing when Snowflake picks broadcast vs shuffle join and how to force each is a senior-level probe.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;th&gt;Join type&lt;/th&gt;
&lt;th&gt;Remote spill&lt;/th&gt;
&lt;th&gt;Wall clock&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Broadcast&lt;/td&gt;
&lt;td&gt;8 GB&lt;/td&gt;
&lt;td&gt;4 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A: NO_BROADCAST&lt;/td&gt;
&lt;td&gt;Hash / Shuffle&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;2 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B: filter dim&lt;/td&gt;
&lt;td&gt;Broadcast (smaller)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;90 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B + C&lt;/td&gt;
&lt;td&gt;Broadcast + probe pruning&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;45 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Broadcast join is optimal for genuinely small dim tables (&amp;lt; 10 K rows). When the "small" side grows past ~1 M rows, force shuffle join or filter the dim first.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;snowflake query profile&lt;/code&gt; interview question on plan reading and warehouse sizing
&lt;/h3&gt;

&lt;p&gt;A senior interviewer often asks: "A finance dashboard on Snowflake takes 8 minutes to load. You're the on-call DE. Walk me through the Query Profile — what do you look at first, what's the ordered checklist of fixes, and how do you decide when to bump the warehouse vs when to rewrite the query?"&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using the top-down profile checklist with pruning, spill, and operator time %
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Step 1 — find the query and its query_id&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;query_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;execution_time&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;seconds&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;SNOWFLAKE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INFORMATION_SCHEMA&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QUERY_HISTORY&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;start_time&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;DATEADD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;execution_time&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 2 — pull the per-operator stats&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GET_QUERY_OPERATOR_STATS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'abc123-def456'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;execution_time_breakdown_overall_percentage&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 3 — check pruning on TableScans&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;operator_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;operator_attributes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;operator_statistics&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;pruning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;partitions_scanned&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;scanned&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;operator_statistics&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;pruning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;partitions_total&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_partitions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;operator_statistics&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;pruning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;partitions_scanned&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;
       &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;operator_statistics&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;pruning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;partitions_total&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;pruning_pct&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GET_QUERY_OPERATOR_STATS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'abc123-def456'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;operator_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'TableScan'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 4 — check spillage&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;bytes_spilled_to_local_storage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;bytes_spilled_to_remote_storage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;warehouse_size&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;SNOWFLAKE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INFORMATION_SCHEMA&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QUERY_HISTORY&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;query_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'abc123-def456'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Find query_id from QUERY_HISTORY&lt;/td&gt;
&lt;td&gt;Isolate the slow query&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;List operators by execution time %&lt;/td&gt;
&lt;td&gt;Identify the hot operator (usually TableScan or Sort/Join)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Check pruning % on all TableScans&lt;/td&gt;
&lt;td&gt;If any &amp;lt; 50%, cluster or rewrite filter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Check remote spill in QUERY_HISTORY&lt;/td&gt;
&lt;td&gt;If non-zero, bump warehouse OR rewrite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Check Broadcast joins on non-small tables&lt;/td&gt;
&lt;td&gt;Force NO_BROADCAST hint or filter first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Verify &lt;code&gt;SELECT *&lt;/code&gt; isn't the culprit&lt;/td&gt;
&lt;td&gt;Project only needed columns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Re-run and diff&lt;/td&gt;
&lt;td&gt;Confirm expected drop in wall clock&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Root cause&lt;/th&gt;
&lt;th&gt;One-line fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pruning % &amp;lt; 50% on filter column&lt;/td&gt;
&lt;td&gt;Table not clustered&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ALTER TABLE t CLUSTER BY (col)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remote spill &amp;gt; 0&lt;/td&gt;
&lt;td&gt;Warehouse too small OR bad plan&lt;/td&gt;
&lt;td&gt;Bump 1 tier OR rewrite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Broadcast join with build &amp;gt; 1M rows&lt;/td&gt;
&lt;td&gt;Stale stats or growing dim&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/*+ NO_BROADCAST(t) */&lt;/code&gt; hint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;SELECT *&lt;/code&gt; on 100-column table&lt;/td&gt;
&lt;td&gt;Excess columnar reads&lt;/td&gt;
&lt;td&gt;Project only needed columns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sort spilling on top-N&lt;/td&gt;
&lt;td&gt;No LIMIT push-down&lt;/td&gt;
&lt;td&gt;Add explicit LIMIT + pre-aggregate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Top-down operator time %&lt;/strong&gt;&lt;/strong&gt; — Snowflake profiles are naturally ordered by execution time %. The hot operator jumps to the top; you don't have to hunt for it. Read from there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Pruning % is the pre-scan lever&lt;/strong&gt;&lt;/strong&gt; — every byte you don't scan is free. Clustering shifts the pruning % from 0 to 99% for the filter column. That single move often 10×'s dashboard queries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Remote spill = memory pressure&lt;/strong&gt;&lt;/strong&gt; — remote is 10–100× slower than local spill. Zero remote spill is the sizing target. When bumping a warehouse takes remote spill to zero, you've found the right tier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Rewrite &amp;gt; scale, when possible&lt;/strong&gt;&lt;/strong&gt; — rewriting (adding LIMIT, pre-aggregating, filtering dims first) is durable and doesn't inflate cost per query. Bumping warehouse is fast but pays 2× per tier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — profile diagnosis is $0 (a few metadata queries). Fix costs vary — clustering a 200 TB table takes credits to reclusters; bumping a warehouse for one query is fractional cost; rewriting the SQL is free but takes engineer-time. Net-cost usually favours rewrite &amp;gt; cluster &amp;gt; scale.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — optimization&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL optimization drills&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/optimization" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — aggregation&lt;/span&gt;
&lt;strong&gt;Pre-aggregation and GROUP BY drills&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/aggregation" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  4. BigQuery execution graph
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;bigquery execution plan&lt;/code&gt; — the stage-based DAG, slot-milliseconds, shuffle bytes, &lt;code&gt;--dry_run&lt;/code&gt; cost estimation, and &lt;code&gt;INFORMATION_SCHEMA.JOBS&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;BigQuery's execution model is a directed acyclic graph of stages, where each stage is a set of parallel workers reading input, applying a piece of the query, and writing output that gets shuffled into the next stage; reading the graph is a matter of finding the hot stage (largest &lt;code&gt;slot_ms&lt;/code&gt;), checking shuffle bytes (network cost), and reading the &lt;code&gt;INFORMATION_SCHEMA.JOBS&lt;/code&gt; row for the query to correlate slot time against wait time&lt;/strong&gt;. Once you can read the stage graph and the JOBS row, you can size reservations, spot skew, and translate cost per query into cost per week.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq1tnhjbgl940gkxyozlc.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq1tnhjbgl940gkxyozlc.jpeg" alt="Visual diagram of the BigQuery execution graph — left a horizontal stage-based DAG with input / compute / output nodes annotated with slot_ms and shuffle records, centre a slot-utilisation vs wait-time bar chart with a queue vs CPU annotation, right an INFORMATION_SCHEMA.JOBS lookup card showing dry_run and total_slot_ms fields; on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Slot 1 — the stage-based model.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stages are the unit of parallel work.&lt;/strong&gt; Each stage has an input (from disk or a previous stage's shuffle output), a compute step (filter, aggregate, join), and a shuffle output. Stages run in parallel wherever their inputs are ready.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slots are the compute unit.&lt;/strong&gt; Each stage runs on some number of slots (BigQuery's word for "worker threads"). On-demand pricing gives 2000 slots per project by default; flat-rate reservations pin exact slot counts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slot-milliseconds (&lt;code&gt;slot_ms&lt;/code&gt;)&lt;/strong&gt; — the fundamental unit of BigQuery compute cost. If a stage uses 100 slots for 4.2 seconds, that's &lt;code&gt;100 × 4200 = 420,000 slot_ms&lt;/code&gt;. The whole-query &lt;code&gt;total_slot_ms&lt;/code&gt; is the sum across all stages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wall clock ≠ slot time.&lt;/strong&gt; Wall clock is what the user waits. Slot time is total compute. A stage using 100 slots for 4.2 s has 420,000 slot_ms but only 4.2 s of wall clock. High parallelism means wall clock much less than slot time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 2 — reading the stage graph.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Console view&lt;/strong&gt; — BigQuery UI → Query History → click query → Execution details tab. Shows the DAG with each stage's slot time, wait time, and shuffle bytes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stage columns.&lt;/strong&gt; &lt;code&gt;slot_ms&lt;/code&gt;, &lt;code&gt;wait_ms&lt;/code&gt;, &lt;code&gt;read_ms&lt;/code&gt;, &lt;code&gt;compute_ms&lt;/code&gt;, &lt;code&gt;write_ms&lt;/code&gt;, &lt;code&gt;shuffle_output_bytes&lt;/code&gt;, &lt;code&gt;records_read&lt;/code&gt;, &lt;code&gt;records_written&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skew signal.&lt;/strong&gt; If a stage has &lt;code&gt;slot_ms&lt;/code&gt; significantly larger than expected given &lt;code&gt;records_read&lt;/code&gt;, the workers are unbalanced — some workers read 100× more than others. Skew is expensive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wait vs compute.&lt;/strong&gt; &lt;code&gt;wait_ms &amp;gt; compute_ms&lt;/code&gt; means the stage was queued waiting for slots (concurrency pressure). &lt;code&gt;compute_ms &amp;gt; wait_ms&lt;/code&gt; means the query was compute-bound (worked flat out).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hot stage.&lt;/strong&gt; The stage with the largest &lt;code&gt;slot_ms&lt;/code&gt; is the bottleneck. Optimising a stage with 200,000 slot_ms of a 220,000-total is worthwhile; optimising a 200-slot_ms stage isn't.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 3 — &lt;code&gt;--dry_run&lt;/code&gt; for cost estimation without execution.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;bq query --dry_run --use_legacy_sql=false 'SELECT ...'&lt;/code&gt;&lt;/strong&gt; — returns the estimated bytes processed without running the query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--dry_run&lt;/code&gt; is free.&lt;/strong&gt; No slots used, no bytes billed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Estimate accuracy.&lt;/strong&gt; Usually within 5% of actual on partition-pruned queries. Larger error on complex joins where the planner can't estimate output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use case — cost gates in CI.&lt;/strong&gt; Wrap queries in a dry-run + assertion: fail if estimated bytes exceed a threshold. Catches accidental &lt;code&gt;SELECT *&lt;/code&gt; on a 100 TB table before deployment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web console equivalent.&lt;/strong&gt; The query editor shows "This query will process X TB when run" — same estimator.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 4 — &lt;code&gt;INFORMATION_SCHEMA.JOBS&lt;/code&gt; audit trail.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SELECT * FROM \&lt;/code&gt;region-us&lt;code&gt;.INFORMATION_SCHEMA.JOBS WHERE job_id = 'abc';&lt;/code&gt;&lt;/strong&gt; — full metadata row per query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key columns.&lt;/strong&gt; &lt;code&gt;total_bytes_processed&lt;/code&gt;, &lt;code&gt;total_slot_ms&lt;/code&gt;, &lt;code&gt;total_bytes_billed&lt;/code&gt;, &lt;code&gt;start_time&lt;/code&gt;, &lt;code&gt;end_time&lt;/code&gt;, &lt;code&gt;cache_hit&lt;/code&gt;, &lt;code&gt;statement_type&lt;/code&gt;, &lt;code&gt;query&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;job_stages&lt;/code&gt;&lt;/strong&gt; — nested array with per-stage details.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;referenced_tables&lt;/code&gt;&lt;/strong&gt; — array of tables the query touched. Useful for lineage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;INFORMATION_SCHEMA.JOBS_BY_ORGANIZATION&lt;/code&gt;&lt;/strong&gt; — organisation-wide view (requires admin role).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retention.&lt;/strong&gt; ~180 days. Sufficient for weekly cost reviews.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 5 — slot utilisation vs wait time (the CPU vs queue distinction).&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compute time = time slots actually worked.&lt;/strong&gt; Green area in the console utilisation chart.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wait time = time slots were assigned but blocked (usually on I/O or shuffle).&lt;/strong&gt; Amber area.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read time = time reading from disk / column storage.&lt;/strong&gt; Grey area.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write time = time writing shuffle output.&lt;/strong&gt; Blue area.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diagnosis reading.&lt;/strong&gt; If wait time &amp;gt; compute time, the query is I/O-bound or shuffle-bound; a bigger reservation won't help. If compute time &amp;gt; wait time, more slots would speed it up (assuming you have them).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 6 — on-demand vs flat-rate slot billing.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On-demand.&lt;/strong&gt; $5 per TB scanned (US region). Bills by &lt;code&gt;total_bytes_billed&lt;/code&gt;. Slots are elastic — up to 2000 per project. Best for spiky workloads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flat-rate reservations.&lt;/strong&gt; Commit to X slots per month. Predictable bill. Queries wait in queue when they exceed the reservation. Best for consistent workloads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BI Engine.&lt;/strong&gt; In-memory cache for dashboarding queries. Reduces slot cost when the same query runs repeatedly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage cost is separate.&lt;/strong&gt; ~$20/TB/month for active storage; ~$10/TB/month for long-term (unchanged for 90 days).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Egress cost is separate.&lt;/strong&gt; Charged per byte leaving GCP.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 7 — reading the graph in five moves.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Move 1 — find the hot stage.&lt;/strong&gt; Sort by &lt;code&gt;slot_ms&lt;/code&gt; desc. The top stage is the bottleneck.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move 2 — check hot stage's compute vs wait.&lt;/strong&gt; Compute &amp;gt; wait = more slots would help. Wait &amp;gt; compute = I/O bound.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move 3 — check shuffle bytes.&lt;/strong&gt; Large shuffle between two stages means the intermediate result is big. Aggregate earlier if possible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move 4 — check &lt;code&gt;records_read&lt;/code&gt; vs partition prune.&lt;/strong&gt; If a stage read 1 T records but the table is 100 T records, pruning worked. If it read 100 T, pruning failed — check partition and cluster keys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move 5 — name the fix.&lt;/strong&gt; Partition the table on the filter column, cluster on high-cardinality keys, rewrite &lt;code&gt;SELECT *&lt;/code&gt;, use approximate aggregations for cardinality estimates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 8 — the eight most common BigQuery execution-graph pathologies.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Full-table scan on a partitioned table.&lt;/strong&gt; Filter isn't partition-pruning-friendly. &lt;code&gt;WHERE DATE(created_at) = '2026-07-10'&lt;/code&gt; on a table partitioned by &lt;code&gt;DATE(created_at)&lt;/code&gt; prunes. &lt;code&gt;WHERE EXTRACT(YEAR FROM created_at) = 2026&lt;/code&gt; doesn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SELECT *&lt;/code&gt; on a wide table.&lt;/strong&gt; Every column is read even if only three are used. Fix — project only what you need.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skewed join.&lt;/strong&gt; One key value has 100× more rows than others; one worker gets stuck. Fix — pre-aggregate, sample, or handle skew explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross join without predicate.&lt;/strong&gt; The classic &lt;code&gt;FROM a, b WHERE a.x = b.x&lt;/code&gt; typo. BigQuery aborts on huge cross joins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Large shuffle between stages.&lt;/strong&gt; The intermediate is bigger than either input. Sometimes fine (aggregation reduces later); usually a bad plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache miss on repeated dashboarding queries.&lt;/strong&gt; BI Engine not configured. Fix — add a reservation with BI Engine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On-demand querying a 100 TB table daily.&lt;/strong&gt; $500/query. Fix — partition, cluster, materialised view, or move to flat-rate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slot wait time &amp;gt; compute time.&lt;/strong&gt; Queue pressure. Fix — larger reservation, or run job in a less contended time window.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 9 — interview probes on BigQuery plan reading.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"How do you read a BigQuery execution plan?"&lt;/strong&gt; — Open Query History, click into query, open Execution details. Find hot stage by slot_ms. Check compute vs wait. Diagnose shuffle. Name fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"What's slot_ms?"&lt;/strong&gt; — Slot × milliseconds. The fundamental compute unit. Total &lt;code&gt;slot_ms&lt;/code&gt; = sum across all stages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"When would you use &lt;code&gt;--dry_run&lt;/code&gt;?"&lt;/strong&gt; — For cost estimation before running expensive queries. Also in CI to gate PRs that would scan too much.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"How do you use &lt;code&gt;INFORMATION_SCHEMA.JOBS&lt;/code&gt;?"&lt;/strong&gt; — Query it for slow-query audits, cost attribution, or plan history. Correlates well with team dashboards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"On-demand or flat-rate?"&lt;/strong&gt; — On-demand for spiky ad-hoc analytics ($5/TB is fine when volume is modest). Flat-rate for consistent workloads where you can commit to steady slot usage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;Common beginner mistakes&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Confusing slot_ms with wall clock — 100 slots × 4 s = 400 slot_s, but wall clock was 4 s.&lt;/li&gt;
&lt;li&gt;Forgetting to filter on the partition column — full-table scan on a partitioned table costs the same as an unpartitioned table.&lt;/li&gt;
&lt;li&gt;Using &lt;code&gt;SELECT *&lt;/code&gt; in dashboards — every extra column is a byte scanned = a cent billed.&lt;/li&gt;
&lt;li&gt;Ignoring shuffle bytes — the intermediate stages may be bigger than the input if the plan is bad.&lt;/li&gt;
&lt;li&gt;Running expensive queries without &lt;code&gt;--dry_run&lt;/code&gt; first.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — the full-table scan on a partitioned table
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The most common BigQuery bill surprise: a table partitioned by &lt;code&gt;DATE(created_at)&lt;/code&gt; but the query filter is &lt;code&gt;WHERE EXTRACT(YEAR FROM created_at) = 2026&lt;/code&gt; — the filter can't prune partitions because the planner can't inverse-map the function.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given the query below on a 100 TB &lt;code&gt;events&lt;/code&gt; table partitioned by &lt;code&gt;DATE(created_at)&lt;/code&gt;, why does &lt;code&gt;--dry_run&lt;/code&gt; estimate 100 TB? What's the fix?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;`project.dataset.events`&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;EXTRACT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;YEAR&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2026&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;EXTRACT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;MONTH&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Diagnose&lt;/span&gt;
&lt;span class="n"&gt;bq&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="c1"&gt;--dry_run --use_legacy_sql=false '&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;`project.dataset.events`&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;EXTRACT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;YEAR&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2026&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;EXTRACT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;MONTH&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="s1"&gt;'
-- Output: This query will process 100 TB when run.

-- Fix: rewrite to a sargable partition filter
SELECT user_id, COUNT(*) AS n
FROM `project.dataset.events`
WHERE created_at &amp;gt;= '&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;07&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;01&lt;/span&gt;&lt;span class="s1"&gt;'
  AND created_at &amp;lt;  '&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;08&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;01&lt;/span&gt;&lt;span class="s1"&gt;'
GROUP BY user_id
LIMIT 100;
-- Dry run: This query will process 3 TB when run. (One month of 100 TB × 12.)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Original query wraps &lt;code&gt;created_at&lt;/code&gt; in &lt;code&gt;EXTRACT(...)&lt;/code&gt;. BigQuery's partition pruner can't invert the function to identify which partitions match — it falls back to full-table scan.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--dry_run&lt;/code&gt; reveals the damage: 100 TB estimated. At $5/TB on-demand, that's $500 per run.&lt;/li&gt;
&lt;li&gt;Fix — rewrite the filter as a range on the raw column: &lt;code&gt;WHERE created_at &amp;gt;= '2026-07-01' AND created_at &amp;lt; '2026-08-01'&lt;/code&gt;. The partition pruner can now identify exactly the July 2026 partitions.&lt;/li&gt;
&lt;li&gt;Re-run &lt;code&gt;--dry_run&lt;/code&gt;: 3 TB estimated. At $5/TB, that's $15 per run — 30× cheaper.&lt;/li&gt;
&lt;li&gt;Rule of thumb — always filter partitioned tables on the raw partition column with &lt;code&gt;&amp;gt;=&lt;/code&gt; / &lt;code&gt;&amp;lt;&lt;/code&gt; / &lt;code&gt;BETWEEN&lt;/code&gt;. Never wrap in a function.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query&lt;/th&gt;
&lt;th&gt;Dry-run bytes&lt;/th&gt;
&lt;th&gt;On-demand cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WHERE EXTRACT(YEAR FROM ...)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;100 TB&lt;/td&gt;
&lt;td&gt;$500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WHERE created_at BETWEEN ...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3 TB&lt;/td&gt;
&lt;td&gt;$15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WHERE created_at BETWEEN ... AND cluster_col = X&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;300 GB&lt;/td&gt;
&lt;td&gt;$1.50&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Always &lt;code&gt;--dry_run&lt;/code&gt; before running big-table queries. Always filter on the partition column with sargable predicates. Cluster the table on the next-most-common filter column for even more pruning.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the skewed join that dominates one stage
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A join where one key value has 100× more rows than others creates a skewed stage — one worker gets stuck processing the hot key while others finish quickly. Symptom: one stage's &lt;code&gt;slot_ms&lt;/code&gt; is much larger than expected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given the execution graph fragment below showing a skewed join stage, name the diagnosis and the fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Stage 3 — HashJoin (fact_events × dim_user)
  Records read:     4,200,000,000
  Records written:  4,200,000,000
  slot_ms:          82,000,000
  wait_ms:          400
  compute_ms:       410,000
  Shuffle output:   660 GB
  Skew warning:     True — user_id=SYSTEM has 800M rows
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Fix A — filter the hot key out (if valid)&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;filtered_events&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;fact_events&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s1"&gt;'SYSTEM'&lt;/span&gt;    &lt;span class="c1"&gt;-- exclude the system pseudo-user&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="s1"&gt;'2026-07-01'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="s1"&gt;'2026-07-31'&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;system_events&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;fact_events&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'SYSTEM'&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="s1"&gt;'2026-07-01'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="s1"&gt;'2026-07-31'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;filtered_events&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;dim_user&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;system_events&lt;/span&gt;   &lt;span class="c1"&gt;-- handle SYSTEM separately&lt;/span&gt;

&lt;span class="c1"&gt;-- Fix B — hash-partition the join key with a salt&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;fact_events&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;dim_user&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;MOD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hash_salt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;MOD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hash_salt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Stage 3 shows 82 M slot_ms — the dominant stage. Records read is 4.2 B, but the skew warning notes one user_id (&lt;code&gt;SYSTEM&lt;/code&gt;) accounts for 800 M rows.&lt;/li&gt;
&lt;li&gt;In a hash join, all rows with the same key hash to the same worker. &lt;code&gt;SYSTEM&lt;/code&gt; sends 800 M rows to one worker while 100+ others finish quickly.&lt;/li&gt;
&lt;li&gt;Fix A — split the hot key. Query the SYSTEM subset separately, then UNION ALL with the non-SYSTEM subset. Each subset's join is balanced.&lt;/li&gt;
&lt;li&gt;Fix B — salt the join. Add a hash-salt column that varies within the hot key, so the 800 M rows spread across 10 workers instead of 1. Requires schema change to the dim table.&lt;/li&gt;
&lt;li&gt;Interview signal — recognising skew from the execution graph and naming a mitigation is a senior-level probe.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;th&gt;slot_ms&lt;/th&gt;
&lt;th&gt;Wall clock&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;82,000,000&lt;/td&gt;
&lt;td&gt;8 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A: filter hot key&lt;/td&gt;
&lt;td&gt;42,000,000&lt;/td&gt;
&lt;td&gt;4 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B: salted key&lt;/td&gt;
&lt;td&gt;44,000,000&lt;/td&gt;
&lt;td&gt;3 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A + partition prune&lt;/td&gt;
&lt;td&gt;8,000,000&lt;/td&gt;
&lt;td&gt;45 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Any stage with slot_ms &amp;gt;&amp;gt; average is a skew candidate. Fix by isolating the hot key or salting it.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — moving from on-demand to a reservation for cost savings
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A team running 50 similar dashboards on-demand at $5/TB every hour discovers they're paying $30,000/month in BigQuery. A flat-rate reservation can cap the bill while providing the same throughput — often 40% cheaper.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given the monthly usage below, decide whether to stay on-demand or switch to flat-rate. What size reservation would break even?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Monthly total bytes billed: 6 PB
Monthly on-demand cost:      $30,000  ($5/TB * 6,000 TB)
Average slot_ms/day:         2.1 * 10^10  (typical 400 slots utilized on avg)
Peak slots concurrent:       ~1200 (short bursts)
Off-peak:                    ~200
Query cadence:               15,000 queries/day
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Estimate flat-rate cost at various commitments&lt;/span&gt;
&lt;span class="c1"&gt;-- Assumes $2000/mo per 100 slots (approximate US pricing; verify current)&lt;/span&gt;
&lt;span class="c1"&gt;-- 500 slots  = $10,000/mo — throttled at peaks&lt;/span&gt;
&lt;span class="c1"&gt;-- 700 slots  = $14,000/mo — few queue stalls&lt;/span&gt;
&lt;span class="c1"&gt;-- 1000 slots = $20,000/mo — covers 95% peaks&lt;/span&gt;
&lt;span class="c1"&gt;-- 1200 slots = $24,000/mo — covers 99% peaks&lt;/span&gt;
&lt;span class="c1"&gt;-- Autoscale up to 500 flex slots = ~$1000/mo extra&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;On-demand at $30 K/month; queries run at up to 1200 slots peak but average 400 slots.&lt;/li&gt;
&lt;li&gt;A 700-slot flat-rate reservation ($14 K/month) covers the average with headroom. Queries can queue during peaks — usually OK for dashboards.&lt;/li&gt;
&lt;li&gt;Add 500 flex-slot autoscaling ($1 K/month typical) to cover the top 5% of peak-slot-demand queries without queueing.&lt;/li&gt;
&lt;li&gt;Total ≈ $15 K/month vs $30 K on-demand — 50% cheaper for the same throughput.&lt;/li&gt;
&lt;li&gt;Trade-off — reservations require a monthly commitment; on-demand is elastic. If usage drops 50% next quarter, on-demand shrinks with it; flat-rate doesn't.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Slots&lt;/th&gt;
&lt;th&gt;Cost / month&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;On-demand&lt;/td&gt;
&lt;td&gt;2000 (elastic)&lt;/td&gt;
&lt;td&gt;$30,000&lt;/td&gt;
&lt;td&gt;Elastic; pays per TB scanned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flat 500 slots&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;$10,000&lt;/td&gt;
&lt;td&gt;Queues at peaks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flat 700 + 500 flex&lt;/td&gt;
&lt;td&gt;700+500&lt;/td&gt;
&lt;td&gt;$15,000&lt;/td&gt;
&lt;td&gt;Sweet spot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flat 1200 slots&lt;/td&gt;
&lt;td&gt;1200&lt;/td&gt;
&lt;td&gt;$24,000&lt;/td&gt;
&lt;td&gt;Almost no queueing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Flat-rate wins when average slot utilisation is above ~30% of peak. On-demand wins for spiky ad-hoc analytics. &lt;code&gt;INFORMATION_SCHEMA.JOBS&lt;/code&gt; gives you the raw material to model this.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;bigquery execution plan&lt;/code&gt; interview question on cost + performance tuning
&lt;/h3&gt;

&lt;p&gt;A senior interviewer often asks: "A dashboard on a 200 TB partitioned + clustered &lt;code&gt;events&lt;/code&gt; table is slow and expensive — 4 minutes per load, $8 per query, running every hour. Walk me through your diagnosis using the execution graph and &lt;code&gt;INFORMATION_SCHEMA.JOBS&lt;/code&gt;, and name the top three fixes ranked by ROI."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using stage-graph diagnosis + partition pruning + materialised view
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Step 1 — pull job metadata&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;job_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;total_bytes_processed&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;POW&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;tb_scanned&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;total_slot_ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;end_time&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start_time&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;wall_clock&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;`region-us`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INFORMATION_SCHEMA&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JOBS_BY_PROJECT&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;job_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'abc123'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 2 — read the stage graph via query_plan (JSON)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;stage_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;slot_ms&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;stage_slot_ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;records_read&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;records_read&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shuffle_output_bytes&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;shuffle_bytes&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;`region-us`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INFORMATION_SCHEMA&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JOBS_BY_PROJECT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;UNNEST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job_stages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;job_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'abc123'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;slot_ms&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Fix A — narrow the partition filter&lt;/span&gt;
&lt;span class="c1"&gt;-- Was: WHERE created_at &amp;gt;= DATE_SUB(CURRENT_DATE(), INTERVAL 90 DAY)&lt;/span&gt;
&lt;span class="c1"&gt;-- Now: WHERE created_at &amp;gt;= DATE_SUB(CURRENT_DATE(), INTERVAL 7 DAY)&lt;/span&gt;
&lt;span class="c1"&gt;-- Effect: 200TB → 15TB scanned per query&lt;/span&gt;

&lt;span class="c1"&gt;-- Fix B — pre-aggregate into a materialised view&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="nv"&gt;`project.dataset.mv_daily_events`&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;revenue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;revenue&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;`project.dataset.events`&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Dashboard now queries the MV, scanning ~10 MB per day per user instead of raw events&lt;/span&gt;

&lt;span class="c1"&gt;-- Fix C — add BI Engine to the reservation&lt;/span&gt;
&lt;span class="c1"&gt;-- (Console → BigQuery → Reservations → Add BI Engine capacity — e.g. 20 GB)&lt;/span&gt;
&lt;span class="c1"&gt;-- Cached repeat queries served in milliseconds&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;th&gt;Bytes scanned&lt;/th&gt;
&lt;th&gt;Slot_ms&lt;/th&gt;
&lt;th&gt;Wall clock&lt;/th&gt;
&lt;th&gt;Cost / query&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Baseline (90-day filter, raw events)&lt;/td&gt;
&lt;td&gt;200 TB&lt;/td&gt;
&lt;td&gt;15 * 10^10&lt;/td&gt;
&lt;td&gt;4 min&lt;/td&gt;
&lt;td&gt;$1,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A — narrow to 7 days&lt;/td&gt;
&lt;td&gt;15 TB&lt;/td&gt;
&lt;td&gt;1.3 * 10^10&lt;/td&gt;
&lt;td&gt;45 s&lt;/td&gt;
&lt;td&gt;$75&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A + B — MV pre-aggregation&lt;/td&gt;
&lt;td&gt;300 MB&lt;/td&gt;
&lt;td&gt;1 * 10^8&lt;/td&gt;
&lt;td&gt;3 s&lt;/td&gt;
&lt;td&gt;$0.02&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A + B + C — BI Engine cache&lt;/td&gt;
&lt;td&gt;300 MB (first) / 0 (cache)&lt;/td&gt;
&lt;td&gt;~0&lt;/td&gt;
&lt;td&gt;200 ms&lt;/td&gt;
&lt;td&gt;$0 (cache hit)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Baseline&lt;/th&gt;
&lt;th&gt;Optimised&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Wall clock&lt;/td&gt;
&lt;td&gt;4 min&lt;/td&gt;
&lt;td&gt;200 ms (cache) / 3 s (miss)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bytes scanned&lt;/td&gt;
&lt;td&gt;200 TB&lt;/td&gt;
&lt;td&gt;300 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost / query&lt;/td&gt;
&lt;td&gt;$1,000&lt;/td&gt;
&lt;td&gt;$0.02&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hourly cost&lt;/td&gt;
&lt;td&gt;$1,000/hr&lt;/td&gt;
&lt;td&gt;$0.02/hr&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weekly cost&lt;/td&gt;
&lt;td&gt;$168,000/wk&lt;/td&gt;
&lt;td&gt;$3.36/wk&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Partition pruning by narrowing the filter window&lt;/strong&gt;&lt;/strong&gt; — most dashboards actually only need the last 7 or 30 days. Narrowing from 90 days to 7 days is a mechanical 13× reduction in bytes scanned. Verify with &lt;code&gt;--dry_run&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Materialised view for pre-aggregation&lt;/strong&gt;&lt;/strong&gt; — dashboards summarise the raw data. Pre-summarise once, query many times. Automatic incremental refresh keeps the MV current. Bytes scanned per dashboard load drops from TB to MB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;BI Engine cache&lt;/strong&gt;&lt;/strong&gt; — an in-memory cache for repeat queries. Sub-second latency; zero bytes scanned on cache hit. Perfect for dashboards where the same query runs every hour.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;&lt;code&gt;INFORMATION_SCHEMA.JOBS&lt;/code&gt; for cost attribution&lt;/strong&gt;&lt;/strong&gt; — the metadata table lets you attribute per-team, per-project, and per-user costs. Weekly review of the top 20 queries by &lt;code&gt;total_bytes_processed&lt;/code&gt; finds the biggest optimisation targets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — Diagnosis: $0 (metadata queries). Fixes: MV creation is a one-off compute cost (~$100 for 200 TB); BI Engine adds a fixed monthly cost (~$100 for 20 GB). Ongoing: 50,000× cheaper. ROI in hours.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — optimization&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL optimization drills&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/optimization" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — window functions&lt;/span&gt;
&lt;strong&gt;Window function and aggregation drills&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/window-functions" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  5. Dialect matrix + 5-step reading strategy
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;read execution plan&lt;/code&gt; across four engines — Postgres, Snowflake, BigQuery, SQL Server — one query, four artefacts, one senior reading strategy
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;the same conceptual query (join, filter, aggregate, sort, limit) compiles to a nested plan tree on Postgres, an operator stack on Snowflake, a stage DAG on BigQuery, and an XML plan graph on SQL Server; the artefacts look different but the reading discipline is the same — find the hot node, check the estimate-vs-actual, check for spill or shuffle, check parallelism, name the fix&lt;/strong&gt;. Learn the five-step discipline once; apply it everywhere.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1fr20m6r03gqvl75yql7.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1fr20m6r03gqvl75yql7.jpeg" alt="Visual diagram of the plan-reading strategy across dialects — left a 4-column dialect matrix (Postgres, Snowflake, BigQuery, SQL Server) showing the EXPLAIN keyword and the profile artefact for each, right a five-step read strategy card (hot node, estimate vs actual, spill or shuffle, parallelism, fix) with numbered ribbons; on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Slot 1 — the dialect matrix.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;th&gt;EXPLAIN keyword&lt;/th&gt;
&lt;th&gt;Profile artefact&lt;/th&gt;
&lt;th&gt;Cost surface&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Postgres&lt;/td&gt;
&lt;td&gt;&lt;code&gt;EXPLAIN (ANALYZE, BUFFERS, VERBOSE, FORMAT TEXT/JSON)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Nested plan tree with per-node cost / rows / actual / loops / buffers&lt;/td&gt;
&lt;td&gt;pg_stat_statements, log_min_duration_statement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snowflake&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;EXPLAIN&lt;/code&gt; (plan only) + Query Profile UI (actual)&lt;/td&gt;
&lt;td&gt;Operator stack with pruning % + spill panel&lt;/td&gt;
&lt;td&gt;QUERY_HISTORY, ACCOUNT_USAGE, GET_QUERY_OPERATOR_STATS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BigQuery&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;bq query --dry_run&lt;/code&gt; + Execution details tab&lt;/td&gt;
&lt;td&gt;Stage DAG with slot_ms + shuffle_bytes&lt;/td&gt;
&lt;td&gt;INFORMATION_SCHEMA.JOBS, dry_run cost estimator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL Server&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SET STATISTICS TIME/IO ON&lt;/code&gt; + Actual Execution Plan (XML)&lt;/td&gt;
&lt;td&gt;Graphical plan in SSMS + XML&lt;/td&gt;
&lt;td&gt;Query Store, DMVs (sys.dm_exec_query_stats)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Slot 2 — SQL Server-specific plan reading.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SET STATISTICS TIME ON&lt;/code&gt;&lt;/strong&gt; — reports CPU and elapsed time per statement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SET STATISTICS IO ON&lt;/code&gt;&lt;/strong&gt; — reports logical reads (from cache) and physical reads (from disk) per table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Actual Execution Plan (Ctrl+M in SSMS)&lt;/strong&gt; — the graphical plan tree. Nodes are labelled with cost %, actual row count, and estimated row count.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Estimated vs Actual Plan&lt;/strong&gt; — Estimated is planning-only (no execution); Actual is post-execution. Prefer Actual for diagnosis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan XML&lt;/strong&gt; — the underlying representation. Machine-readable; can be diffed programmatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Query Store&lt;/strong&gt; — SQL Server's built-in query performance history. Similar to &lt;code&gt;pg_stat_statements&lt;/code&gt; but with plan diffs and forced-plan capability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DMVs&lt;/strong&gt; — &lt;code&gt;sys.dm_exec_query_stats&lt;/code&gt;, &lt;code&gt;sys.dm_exec_query_plan&lt;/code&gt;, &lt;code&gt;sys.dm_os_wait_stats&lt;/code&gt;. Real-time perf data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing Index DMVs&lt;/strong&gt; — &lt;code&gt;sys.dm_db_missing_index_details&lt;/code&gt; — SQL Server's suggestion engine. Use as a starting point, not a truth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Common operators.&lt;/strong&gt; &lt;code&gt;Index Seek&lt;/code&gt;, &lt;code&gt;Clustered Index Scan&lt;/code&gt;, &lt;code&gt;Hash Match&lt;/code&gt;, &lt;code&gt;Nested Loops&lt;/code&gt;, &lt;code&gt;Sort&lt;/code&gt;, &lt;code&gt;Stream Aggregate&lt;/code&gt;, &lt;code&gt;Table Spool&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost estimate.&lt;/strong&gt; Each operator has a "cost of subtree" %. The percentages sum to 100%; the operator with the highest % is the bottleneck.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 3 — the same query across four engines.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Same conceptual query on all four engines&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;lifetime_value&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2026-06-10'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;lifetime_value&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each engine shows this differently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Postgres&lt;/strong&gt; — nested tree: &lt;code&gt;Limit → Sort → HashAggregate → Bitmap Heap Scan on orders&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snowflake&lt;/strong&gt; — operator stack: &lt;code&gt;Result → Sort → Aggregate → Filter → TableScan(orders)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BigQuery&lt;/strong&gt; — 3-stage DAG: &lt;code&gt;Stage 1 (read + filter) → Shuffle → Stage 2 (aggregate + sort) → Stage 3 (limit + output)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL Server&lt;/strong&gt; — XML plan tree with &lt;code&gt;Limit → Sort → Hash Match (Aggregate) → Index Seek on orders(created_at)&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 4 — the 5-step senior read strategy (applies to all four engines).&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Step 1 — find the hot node.&lt;/strong&gt; The one with the largest fraction of total wall clock (or slot_ms on BigQuery). Every profile UI sorts by this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 2 — check estimate vs actual.&lt;/strong&gt; Every plan has an estimate and an actual row count per node. If they diverge by 10× or more, the optimiser was wrong — refresh stats or add extended stats.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 3 — check spill or shuffle.&lt;/strong&gt; Postgres &lt;code&gt;Buffers: temp read&lt;/code&gt;. Snowflake &lt;code&gt;bytes_spilled_to_remote_storage&lt;/code&gt;. BigQuery &lt;code&gt;shuffle_bytes&lt;/code&gt; (large intermediate). SQL Server &lt;code&gt;Sort Warning&lt;/code&gt; in the XML plan. Any of these is a specific fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 4 — check parallelism.&lt;/strong&gt; Postgres &lt;code&gt;Workers Launched&lt;/code&gt;. Snowflake warehouse size. BigQuery slot utilisation vs wait time. SQL Server &lt;code&gt;MAXDOP&lt;/code&gt; setting. If parallelism is disabled or unused, decide whether it should be.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 5 — fix the plan.&lt;/strong&gt; Add index, run ANALYZE, rewrite join, add hint, bump memory, cluster the table, pre-aggregate. The hot-node diagnosis names the one-line fix.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 5 — hints and last-resort tools.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Postgres — &lt;code&gt;pg_hint_plan&lt;/code&gt;.&lt;/strong&gt; An extension for query hints. &lt;code&gt;/*+ SeqScan(t) */&lt;/code&gt; forces seq scan. Use rarely — hints lock the plan against future data shifts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snowflake — &lt;code&gt;NO_BROADCAST&lt;/code&gt;, &lt;code&gt;BROADCAST&lt;/code&gt;, &lt;code&gt;MATERIALIZED_VIEW_HINT&lt;/code&gt;.&lt;/strong&gt; Fewer hints than other engines; the optimiser is intentionally opaque.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BigQuery — no hints per se.&lt;/strong&gt; Rewrite the SQL to force a plan shape. Use materialised views or authorised views for repeated shapes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL Server — many hints.&lt;/strong&gt; &lt;code&gt;WITH (NOLOCK)&lt;/code&gt;, &lt;code&gt;OPTION (HASH JOIN)&lt;/code&gt;, &lt;code&gt;OPTION (MAXDOP 4)&lt;/code&gt;. Powerful but easily abused.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rule.&lt;/strong&gt; Hints are a last resort. First — refresh stats. Second — rewrite. Third — index. Fourth — hint (and add a comment explaining the hint's justification for the next maintainer).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 6 — cost per query (bill-line thinking).&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Postgres self-hosted.&lt;/strong&gt; Cost = servers × hours. Query cost = fraction of server-time. Diagnose to keep utilisation reasonable; scale up when needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres cloud (RDS, Aurora).&lt;/strong&gt; Cost = instance-hours + IO. Bad queries can inflate IO — check &lt;code&gt;pg_stat_statements&lt;/code&gt; and diff top-N by total time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snowflake.&lt;/strong&gt; Cost = warehouse-tier × query-wall-clock. Bad plans = long wall clock = big bill. Optimise for shorter wall clock at a given tier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BigQuery on-demand.&lt;/strong&gt; Cost = TB scanned × $5. Bad plans = full-table scans = big bill. Optimise for pruned partitions and smart clustering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BigQuery flat-rate.&lt;/strong&gt; Cost = slot commitment × month. Bad plans = queue waits or overrun. Optimise for slot efficiency (compute &amp;gt; wait).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL Server.&lt;/strong&gt; Cost = server + license. Query cost = share of server; long queries block others via lock contention. Diagnose lock waits with DMVs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 7 — reading discipline vs tuning discipline.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reading.&lt;/strong&gt; Every plan artefact you can read is a chance to catch a regression before it ships. Add plan diffs to code review. Compare prod vs staging plans for the same query. Set alerts on &lt;code&gt;pg_stat_statements&lt;/code&gt; for queries whose mean-time doubles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tuning.&lt;/strong&gt; Once the read names the fix, apply the fix. Prefer stats refresh (cheap) → query rewrite (durable) → index (durable, mild write cost) → materialised view (durable, storage cost) → warehouse bump (fast, expensive). Hints are last.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 8 — the SQL Server actual execution plan XML example.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- SQL Server: turn on both flags&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;STATISTICS&lt;/span&gt; &lt;span class="nb"&gt;TIME&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;STATISTICS&lt;/span&gt; &lt;span class="n"&gt;IO&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Include Actual Execution Plan (Ctrl+M) then run:&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;lifetime_value&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2026-06-10'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;lifetime_value&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="k"&gt;NEXT&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Right-click the plan → Show Execution Plan XML → paste into a diff tool&lt;/span&gt;

&lt;span class="c1"&gt;-- Turn off after diagnosis&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;STATISTICS&lt;/span&gt; &lt;span class="nb"&gt;TIME&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;STATISTICS&lt;/span&gt; &lt;span class="n"&gt;IO&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Common beginner mistakes&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reading only one engine's plan without recognising the other three shapes.&lt;/li&gt;
&lt;li&gt;Confusing Snowflake's operator stack (top-down) with Postgres's plan tree (bottom-up).&lt;/li&gt;
&lt;li&gt;Forgetting that BigQuery slot_ms ≠ wall clock — a 100-slot query is fast relative to slot time.&lt;/li&gt;
&lt;li&gt;Skipping the estimate-vs-actual check — the single most reliable signal of a bad plan.&lt;/li&gt;
&lt;li&gt;Bumping resources first — always diagnose the plan first; scale is the last resort.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Worked example — reading the same query on all four engines
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The clearest way to lock in the mental model: read the same query's plan on Postgres, Snowflake, BigQuery, and SQL Server, and identify the equivalent nodes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given the query and the plan artefacts from each engine, map each engine's nodes to the conceptual operators (Filter, GroupBy, Sort, Limit).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- The query&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;lifetime_value&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2026-06-10'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;lifetime_value&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt; (No code — a mapping exercise.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Conceptual operator | Postgres node        | Snowflake operator | BigQuery stage | SQL Server node
Read                | Bitmap/Index Scan    | TableScan          | Stage 1 input  | Index Seek
Filter              | Recheck Cond / Filter | Filter / Scan     | Stage 1 filter | (subsumed)
GroupBy             | HashAggregate        | Aggregate          | Stage 2 agg    | Hash Match (Aggregate)
Sort                | Sort                 | Sort               | Stage 2 sort   | Sort
Limit               | Limit                | Result             | Stage 3 output | Top
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read — every engine has a scan. Postgres shows the physical access method (Bitmap / Index / Seq); Snowflake shows partitions scanned; BigQuery shows records_read per stage; SQL Server shows the seek predicate.&lt;/li&gt;
&lt;li&gt;Filter — subsumed into scan on most engines when the predicate is sargable (index-lookup key on Postgres / SQL Server; partition prune on BigQuery; pruning % on Snowflake).&lt;/li&gt;
&lt;li&gt;GroupBy — always a hash aggregate for high-cardinality group keys. Postgres and SQL Server can also pick sort-based aggregation when input is already sorted.&lt;/li&gt;
&lt;li&gt;Sort — the &lt;code&gt;ORDER BY SUM(total) DESC&lt;/code&gt; requires sorting after aggregation. Postgres and SQL Server may pick top-N heapsort when combined with LIMIT.&lt;/li&gt;
&lt;li&gt;Limit — the top node. Postgres &lt;code&gt;Limit&lt;/code&gt;, Snowflake &lt;code&gt;Result&lt;/code&gt; (subsumes limit), BigQuery output stage, SQL Server &lt;code&gt;Top&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt; The mapping in the table above. Learn one column deeply; the others come naturally once you can read one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Read one engine deeply first (usually Postgres because the syntax is most explicit). Then translate to Snowflake, BigQuery, SQL Server as you encounter them. The five-step reading discipline is invariant across all four.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the same query optimised differently on each engine
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Optimising the same conceptual query on each engine involves engine-specific levers — Postgres wants indexes, Snowflake wants clustering, BigQuery wants partition + cluster keys, SQL Server wants an index seek plus a covering include column.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; For the query above, give the one-line optimisation for each engine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- The query&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;lifetime_value&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2026-06-10'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;lifetime_value&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Postgres&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;CONCURRENTLY&lt;/span&gt; &lt;span class="n"&gt;idx_orders_created_customer_total&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;INCLUDE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Snowflake&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;CLUSTER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- BigQuery&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="nv"&gt;`project.dataset.orders`&lt;/span&gt;
&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;CLUSTER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;`project.dataset.orders`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- SQL Server&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;NONCLUSTERED&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_orders_created_at&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;INCLUDE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Postgres — composite index on &lt;code&gt;(created_at, customer_id)&lt;/code&gt; with &lt;code&gt;INCLUDE (total)&lt;/code&gt; covers the filter, the group key, and the aggregate — index-only scan possible.&lt;/li&gt;
&lt;li&gt;Snowflake — cluster by &lt;code&gt;created_at&lt;/code&gt; so the &lt;code&gt;WHERE&lt;/code&gt; filter prunes micro-partitions. &lt;code&gt;SELECT&lt;/code&gt;ing only three columns already benefits from columnar storage.&lt;/li&gt;
&lt;li&gt;BigQuery — partition by &lt;code&gt;DATE(created_at)&lt;/code&gt; so the filter prunes date partitions. Cluster by &lt;code&gt;customer_id&lt;/code&gt; so the group-by benefits from block-level locality.&lt;/li&gt;
&lt;li&gt;SQL Server — nonclustered index on &lt;code&gt;created_at&lt;/code&gt; with &lt;code&gt;INCLUDE (customer_id, total)&lt;/code&gt; — covers the filter and projects the two needed columns.&lt;/li&gt;
&lt;li&gt;All four fixes achieve the same shape: a small scan + hash aggregate + sort + limit. Wall clock drops from tens of seconds to sub-second.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;th&gt;Speed-up&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Postgres&lt;/td&gt;
&lt;td&gt;Seq Scan / 1.2 s&lt;/td&gt;
&lt;td&gt;Index Only Scan / 15 ms&lt;/td&gt;
&lt;td&gt;80×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snowflake&lt;/td&gt;
&lt;td&gt;0% prune / 45 s&lt;/td&gt;
&lt;td&gt;99% prune / 4 s&lt;/td&gt;
&lt;td&gt;11×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BigQuery&lt;/td&gt;
&lt;td&gt;Full-scan / 200 TB&lt;/td&gt;
&lt;td&gt;Partition + cluster / 2 GB&lt;/td&gt;
&lt;td&gt;100,000× cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL Server&lt;/td&gt;
&lt;td&gt;Clustered Index Scan / 2.5 s&lt;/td&gt;
&lt;td&gt;Index Seek + Include / 30 ms&lt;/td&gt;
&lt;td&gt;83×&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Every engine has an equivalent "cover the query with the index" pattern. Learn the syntax for each; the concept is the same.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;execution plan analysis&lt;/code&gt; interview question on cross-engine plan reading
&lt;/h3&gt;

&lt;p&gt;A senior interviewer often asks: "Design a runbook a mid-level engineer can follow to read execution plans across Postgres, Snowflake, BigQuery, and SQL Server. What are the five steps, and what's the one-line signal on each engine that tells you 'this plan is broken'?"&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using the invariant 5-step read strategy plus a per-engine cheat card
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;STEP 1 — find the hot node/stage.
  Postgres:  sort plan by actual_time; deepest actual_time × loops = hot node.
  Snowflake: sort by execution_time_breakdown_overall_percentage; top card = hot.
  BigQuery:  sort by slot_ms; largest stage = hot.
  SQL Server: sort by cost of subtree %; highest = hot.

STEP 2 — check estimate vs actual.
  Postgres:  rows= vs actual rows= per node. Gap ≥ 10× is bad.
  Snowflake: input rows vs output rows on operators. Compare to statistics.
  BigQuery:  records_read vs estimated records. INFO_SCHEMA.JOBS has both.
  SQL Server: Estimated Number of Rows vs Actual Number of Rows on each node.

STEP 3 — check spill or shuffle.
  Postgres:  BUFFERS: temp read non-zero; Sort Method: external merge.
  Snowflake: bytes_spilled_to_remote_storage non-zero.
  BigQuery:  large shuffle_output_bytes between stages.
  SQL Server: Sort Warning; Hash Warning; TempDB usage.

STEP 4 — check parallelism.
  Postgres:  Workers Launched. Should be &amp;gt; 0 on big scans.
  Snowflake: warehouse size and MULTI_STATEMENT_COUNT.
  BigQuery:  slot count and wait_ms.
  SQL Server: MAXDOP and Parallelism operator in the plan.

STEP 5 — fix the plan.
  Postgres:  ANALYZE; add index; rewrite join; SET work_mem session.
  Snowflake: CLUSTER BY; rewrite filter; bump warehouse tier.
  BigQuery:  partition + cluster; materialised view; rewrite SELECT.
  SQL Server: rebuild statistics; CREATE INDEX; Query Store forced plan.

BROKEN-PLAN one-line signals (per engine):
  Postgres:  "Rows Removed by Filter: 99000000" — missing index.
  Snowflake: "Partitions scanned = Partitions total" — no clustering / bad filter.
  BigQuery:  "Full scan on partitioned table" — filter not sargable.
  SQL Server: "Clustered Index Scan on 100M-row table with cost 98%" — missing index.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Postgres&lt;/th&gt;
&lt;th&gt;Snowflake&lt;/th&gt;
&lt;th&gt;BigQuery&lt;/th&gt;
&lt;th&gt;SQL Server&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 hot&lt;/td&gt;
&lt;td&gt;actual_time × loops&lt;/td&gt;
&lt;td&gt;exec time %&lt;/td&gt;
&lt;td&gt;slot_ms per stage&lt;/td&gt;
&lt;td&gt;cost of subtree %&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 est vs act&lt;/td&gt;
&lt;td&gt;rows vs actual rows&lt;/td&gt;
&lt;td&gt;input vs output rows&lt;/td&gt;
&lt;td&gt;records_read&lt;/td&gt;
&lt;td&gt;Est vs Actual rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3 spill&lt;/td&gt;
&lt;td&gt;temp read&lt;/td&gt;
&lt;td&gt;remote spill bytes&lt;/td&gt;
&lt;td&gt;large shuffle&lt;/td&gt;
&lt;td&gt;Sort/Hash warning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 parallel&lt;/td&gt;
&lt;td&gt;Workers Launched&lt;/td&gt;
&lt;td&gt;WH size&lt;/td&gt;
&lt;td&gt;slot count vs wait&lt;/td&gt;
&lt;td&gt;MAXDOP + parallel op&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5 fix&lt;/td&gt;
&lt;td&gt;ANALYZE + INDEX&lt;/td&gt;
&lt;td&gt;CLUSTER + rewrite&lt;/td&gt;
&lt;td&gt;PARTITION + MV&lt;/td&gt;
&lt;td&gt;REBUILD + INDEX&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The runbook is invariant across all four engines. The signals differ; the discipline is the same.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;th&gt;Fastest one-signal diagnosis&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Postgres&lt;/td&gt;
&lt;td&gt;Any &lt;code&gt;Seq Scan&lt;/code&gt; with &lt;code&gt;Rows Removed by Filter&lt;/code&gt; &amp;gt; 1M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snowflake&lt;/td&gt;
&lt;td&gt;Any TableScan with pruning &amp;lt; 50% on a filter column&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BigQuery&lt;/td&gt;
&lt;td&gt;Any query where &lt;code&gt;dry_run&lt;/code&gt; bytes &amp;gt; 10 TB on a partitioned table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL Server&lt;/td&gt;
&lt;td&gt;Any plan node with cost of subtree &amp;gt; 80% and no index seek&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Invariant discipline&lt;/strong&gt;&lt;/strong&gt; — every plan diagnosis reduces to five steps: hot node, estimate vs actual, spill/shuffle, parallelism, fix. The signals differ per engine but the mental model is one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;One-signal diagnosis&lt;/strong&gt;&lt;/strong&gt; — for every engine, there's a single reading that catches the most common plan pathology. Teaching a mid-level engineer that signal gives them 80% of senior plan-reading skill for that engine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Runbook portability&lt;/strong&gt;&lt;/strong&gt; — an engineer who learns the runbook on Postgres can port it to Snowflake or BigQuery in a day. The mental model is the durable asset; syntax is trivia.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Escalation path&lt;/strong&gt;&lt;/strong&gt; — cheap fixes first (refresh stats), durable fixes second (index, cluster, partition), expensive fixes last (scale up, hint, materialise). Applies universally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — the runbook itself is a one-page cheat sheet; teaching it takes an hour. Ongoing cost is engineer-time saved on diagnosis (typically 30–60 min per incident reduced to 5).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — optimization&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL optimization drills&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/optimization" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;


&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — joins&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL join optimisation drills&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/joins" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;





&lt;h2&gt;
  
  
  Cheat sheet — plan-reading recipe list
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bottom-up reading rule.&lt;/strong&gt; On Postgres and SQL Server, always read the plan tree bottom-up — leaf scans first, &lt;code&gt;Limit&lt;/code&gt; last. On Snowflake and BigQuery, sort operators or stages by execution time % / slot_ms and read the top.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost vs actual time.&lt;/strong&gt; &lt;code&gt;cost=&lt;/code&gt; is planner units (Postgres, SQL Server), not milliseconds. &lt;code&gt;actual time=&lt;/code&gt; is wall clock (ms). Never quote cost to non-DBA humans.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The four numbers per Postgres node.&lt;/strong&gt; cost (startup..total), rows (estimate), actual time (first..last), loops. Wall-clock cost = &lt;code&gt;actual time × loops&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Estimate vs actual gap.&lt;/strong&gt; Any node with &lt;code&gt;rows&lt;/code&gt; off by 10×+ from &lt;code&gt;actual rows&lt;/code&gt; is a bad-stats or bad-plan signal. Fix — &lt;code&gt;ANALYZE&lt;/code&gt;, add extended statistics, or force the plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Rows Removed by Filter&lt;/code&gt;.&lt;/strong&gt; Any Postgres &lt;code&gt;Seq Scan&lt;/code&gt; with &lt;code&gt;Rows Removed by Filter&lt;/code&gt; in the millions is a missing-index diagnosis. Add the index.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;BUFFERS: temp read&lt;/code&gt;.&lt;/strong&gt; Non-zero on Postgres = spilling. Fix — rewrite (LIMIT, pre-aggregate) or &lt;code&gt;SET work_mem&lt;/code&gt; per session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Sort Method&lt;/code&gt;.&lt;/strong&gt; Postgres — &lt;code&gt;quicksort&lt;/code&gt; (in-mem), &lt;code&gt;top-N heapsort&lt;/code&gt; (bounded), &lt;code&gt;external merge&lt;/code&gt; (spilled). &lt;code&gt;external merge&lt;/code&gt; = spill = fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Batches: N &amp;gt; 1&lt;/code&gt;.&lt;/strong&gt; Postgres &lt;code&gt;Hash&lt;/code&gt; node with N &amp;gt; 1 = spilling. Fix — bump &lt;code&gt;work_mem&lt;/code&gt; or filter build side down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Workers Launched: 0&lt;/code&gt;.&lt;/strong&gt; Postgres parallelism disabled. Check &lt;code&gt;max_parallel_workers_per_gather&lt;/code&gt; and &lt;code&gt;min_parallel_table_scan_size&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Nested Loop&lt;/code&gt; on two large sides.&lt;/strong&gt; Bad plan almost always. Fix — refresh stats or force &lt;code&gt;Hash Join&lt;/code&gt; (last resort).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snowflake pruning %.&lt;/strong&gt; The single most important number. 99% = great. Under 50% = fix the filter or &lt;code&gt;CLUSTER BY&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snowflake remote spill.&lt;/strong&gt; Non-zero &lt;code&gt;bytes_spilled_to_remote_storage&lt;/code&gt; = warehouse too small OR bad plan. Bump one tier or rewrite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snowflake local spill.&lt;/strong&gt; Non-zero &lt;code&gt;bytes_spilled_to_local_storage&lt;/code&gt; but zero remote = borderline. Not urgent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snowflake &lt;code&gt;SELECT *&lt;/code&gt;.&lt;/strong&gt; Costs more than &lt;code&gt;SELECT a, b, c&lt;/code&gt; — columnar storage. Project only needed columns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snowflake broadcast join.&lt;/strong&gt; Fine for &amp;lt; 10 K rows on the build side. Over ~1 M, force &lt;code&gt;NO_BROADCAST&lt;/code&gt; or filter first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BigQuery slot_ms.&lt;/strong&gt; Fundamental compute cost unit. Sort stages by slot_ms desc to find the hot stage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BigQuery &lt;code&gt;--dry_run&lt;/code&gt;.&lt;/strong&gt; Free bytes-processed estimate. Always run before expensive queries. Gate PRs in CI on estimated bytes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BigQuery partition pruning.&lt;/strong&gt; Filter on the raw partition column with &lt;code&gt;&amp;gt;=&lt;/code&gt; / &lt;code&gt;BETWEEN&lt;/code&gt;. Never wrap in a function (&lt;code&gt;EXTRACT&lt;/code&gt;, &lt;code&gt;DATE&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BigQuery clustering.&lt;/strong&gt; Cluster on high-cardinality filter columns after partition. Block-level pruning inside each partition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BigQuery materialised view.&lt;/strong&gt; Pre-aggregate for dashboards. Automatic incremental refresh. Bytes scanned drops 1000×+.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BigQuery on-demand vs flat-rate.&lt;/strong&gt; On-demand ($5/TB) for spiky ad-hoc. Flat-rate (per-slot commitment) for consistent workloads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL Server &lt;code&gt;SET STATISTICS TIME/IO ON&lt;/code&gt;.&lt;/strong&gt; Reports CPU time, elapsed time, logical reads, physical reads. Turn on for diagnosis, off after.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL Server actual execution plan.&lt;/strong&gt; Ctrl+M in SSMS. Cost of subtree % on every node sums to 100%; hottest node is the fix target.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL Server Query Store.&lt;/strong&gt; Historical query perf + plan diffs. Forced plans available.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The 5-step senior read strategy.&lt;/strong&gt; Hot node → estimate vs actual → spill/shuffle → parallelism → fix. Invariant across all four engines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refresh stats first, always.&lt;/strong&gt; &lt;code&gt;ANALYZE&lt;/code&gt; on Postgres. Snowflake refreshes automatically. BigQuery via &lt;code&gt;INFORMATION_SCHEMA&lt;/code&gt;. SQL Server &lt;code&gt;UPDATE STATISTICS&lt;/code&gt;. Fresh stats fix ~30% of bad plans immediately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rewrite before scaling.&lt;/strong&gt; Query rewrite is free forever. Scaling is a monthly cost. Prefer rewrite when feasible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hints are last resort.&lt;/strong&gt; Every hint locks a plan against future data shifts. Prefer index / cluster / partition changes to hints when possible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost per query.&lt;/strong&gt; Postgres self-hosted = server share. Snowflake = warehouse × wall clock. BigQuery on-demand = TB × $5. Flat-rate = monthly commit. Know your cost model when reading plans.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan diffs in code review.&lt;/strong&gt; Add &lt;code&gt;EXPLAIN (ANALYZE, BUFFERS)&lt;/code&gt; outputs to PR descriptions for schema or query changes. Catches regressions before ship.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI cost gates.&lt;/strong&gt; Wrap expensive queries with &lt;code&gt;--dry_run&lt;/code&gt; in CI. Fail PR if estimated bytes exceed a threshold.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring stack.&lt;/strong&gt; Postgres &lt;code&gt;pg_stat_statements&lt;/code&gt;. Snowflake &lt;code&gt;QUERY_HISTORY&lt;/code&gt;. BigQuery &lt;code&gt;INFORMATION_SCHEMA.JOBS&lt;/code&gt;. SQL Server &lt;code&gt;Query Store&lt;/code&gt;. Weekly review of top-N by mean time or total bytes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rule of thumb.&lt;/strong&gt; 80% of bad plans come from 20% of queries. Diagnose the top 20 by cost weekly; that's usually where the leverage is.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is SQL EXPLAIN ANALYZE and how do I read the output?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;sql explain analyze&lt;/code&gt; is the primitive every engine ships for surfacing the physical plan the executor actually walked — on Postgres it's &lt;code&gt;EXPLAIN (ANALYZE, BUFFERS, VERBOSE, FORMAT TEXT)&lt;/code&gt; and it returns a nested plan tree annotated with &lt;code&gt;cost=startup..total&lt;/code&gt; (planner units), &lt;code&gt;rows=N&lt;/code&gt; (estimate), &lt;code&gt;actual time=first..last&lt;/code&gt; (wall clock ms), &lt;code&gt;loops=N&lt;/code&gt; (executions), and optional &lt;code&gt;BUFFERS: shared hit / shared read / temp read&lt;/code&gt; (memory vs disk vs spill). Read it &lt;strong&gt;bottom-up&lt;/strong&gt; — the leaf scans execute first and feed intermediate nodes (joins, aggregates), which feed the top node (&lt;code&gt;Limit&lt;/code&gt;, &lt;code&gt;Sort&lt;/code&gt;); the leaf whose &lt;code&gt;actual time × loops&lt;/code&gt; dominates total wall clock is the bottleneck. The reading discipline is five steps — find the hot node, compare estimate to actual (10×+ gap = stale stats or bad plan), check for spill (&lt;code&gt;Buffers: temp read&lt;/code&gt; non-zero, &lt;code&gt;Sort Method: external merge&lt;/code&gt;), check parallelism (&lt;code&gt;Workers Launched &amp;gt; 0&lt;/code&gt;), and name the fix (add index, run &lt;code&gt;ANALYZE&lt;/code&gt;, rewrite join, bump &lt;code&gt;work_mem&lt;/code&gt;). On Snowflake, the equivalent is the Snowsight Query Profile with operator cards and pruning %; on BigQuery, it's the execution graph with per-stage &lt;code&gt;slot_ms&lt;/code&gt;; on SQL Server, it's the actual execution plan XML with cost-of-subtree percentages. The read is invariant across engines; only the artefact differs.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the difference between Postgres cost and actual time?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Cost&lt;/strong&gt; is a made-up planner unit used only to rank plan alternatives during optimisation — a &lt;code&gt;Seq Scan&lt;/code&gt; might cost &lt;code&gt;0.00..25000.00&lt;/code&gt;, but that does not mean 25 seconds; it means "the CBO's internal price for this scan compared to alternatives." &lt;strong&gt;Actual time&lt;/strong&gt; is wall clock in milliseconds — &lt;code&gt;actual time=42.512..42.548&lt;/code&gt; means the first row from this node was ready 42.512 ms after query start; the last was ready at 42.548 ms. The two are &lt;strong&gt;not comparable&lt;/strong&gt; — cost is arbitrary and only meaningful within a single plan; actual time is real and directly interpretable. On top of that, if a node has &lt;code&gt;loops=1000&lt;/code&gt; it executed a thousand times, so the wall-clock cost of the node is &lt;code&gt;actual time × loops&lt;/code&gt;, not just &lt;code&gt;actual time&lt;/code&gt;. When you're diagnosing a slow query, ignore cost entirely and read &lt;code&gt;actual time × loops&lt;/code&gt; per node; when you're explaining why the optimiser chose plan A over plan B, cost is the axis that matters. Never quote cost to a business stakeholder — "this query cost 250,000" means nothing to them; "this query took 42 seconds" means everything.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I read a Snowflake query profile?
&lt;/h3&gt;

&lt;p&gt;Open &lt;strong&gt;Snowsight → Activity → Query History → click into the query → Profile tab&lt;/strong&gt; — the graphical view shows a stack of operator cards (TableScan, Filter, Aggregate, Join, Sort, WindowFunction), each with &lt;code&gt;Execution time %&lt;/code&gt;, &lt;code&gt;Bytes scanned&lt;/code&gt;, &lt;code&gt;Rows produced&lt;/code&gt;, and per-operator statistics. Read &lt;strong&gt;top-down&lt;/strong&gt;, sorted by execution time %, and the hot operator surfaces first. On the hot card — check &lt;strong&gt;pruning %&lt;/strong&gt; for TableScans (99% is great, under 50% is a clustering or filter problem), check &lt;strong&gt;local vs remote disk spill&lt;/strong&gt; for aggregates and sorts (&lt;code&gt;bytes_spilled_to_remote_storage&lt;/code&gt; non-zero is the strongest single signal that the warehouse is too small), and check &lt;strong&gt;join algorithm&lt;/strong&gt; for joins (Broadcast for genuinely small dim tables, hash / shuffle for large tables). The underlying data lives in &lt;code&gt;SNOWFLAKE.INFORMATION_SCHEMA.QUERY_HISTORY&lt;/code&gt; (near-real-time, 7-day retention) and &lt;code&gt;SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY&lt;/code&gt; (delayed but 1-year retention), with per-operator stats via &lt;code&gt;GET_QUERY_OPERATOR_STATS(query_id)&lt;/code&gt;. The five-step reading discipline is identical to other engines — hot operator, estimate vs actual (input vs output rows), spill (remote &amp;gt; local &amp;gt; none), parallelism (warehouse size), name the fix (&lt;code&gt;CLUSTER BY&lt;/code&gt;, rewrite filter, bump warehouse, force &lt;code&gt;NO_BROADCAST&lt;/code&gt; hint).&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I read a BigQuery execution plan?
&lt;/h3&gt;

&lt;p&gt;Open &lt;strong&gt;BigQuery Console → Query History → click the query → Execution details tab&lt;/strong&gt; — the graphical view shows a &lt;strong&gt;stage-based DAG&lt;/strong&gt; where each stage is a set of parallel workers reading input, applying part of the query, and shuffling output to the next stage. The fundamental compute unit is &lt;code&gt;slot_ms&lt;/code&gt; (slot-milliseconds); a stage using 100 slots for 4.2 s costs 420 000 slot_ms with only 4.2 s of wall clock. Read stages sorted by &lt;code&gt;slot_ms&lt;/code&gt; desc — the top stage is the bottleneck — and inspect &lt;strong&gt;&lt;code&gt;records_read&lt;/code&gt;&lt;/strong&gt; (partition pruning worked if this is a small fraction of the table), &lt;strong&gt;&lt;code&gt;shuffle_output_bytes&lt;/code&gt;&lt;/strong&gt; (large shuffle = big intermediate = optimise upstream), &lt;strong&gt;&lt;code&gt;wait_ms&lt;/code&gt; vs &lt;code&gt;compute_ms&lt;/code&gt;&lt;/strong&gt; (wait &amp;gt; compute = slots queued; compute &amp;gt; wait = flat out), and &lt;strong&gt;skew warnings&lt;/strong&gt; (one hot key monopolising a worker). The underlying data is &lt;code&gt;INFORMATION_SCHEMA.JOBS&lt;/code&gt; with &lt;code&gt;total_slot_ms&lt;/code&gt;, &lt;code&gt;total_bytes_processed&lt;/code&gt;, and &lt;code&gt;job_stages&lt;/code&gt; (nested per-stage details). Use &lt;strong&gt;&lt;code&gt;bq query --dry_run&lt;/code&gt;&lt;/strong&gt; to estimate &lt;code&gt;bytes_processed&lt;/code&gt; before running — free, matches actual within ~5%, and pairs with CI gates to reject expensive PRs. The senior fix set is partition on the filter column (&lt;code&gt;PARTITION BY DATE(created_at)&lt;/code&gt;), cluster on the next-most-common filter (&lt;code&gt;CLUSTER BY user_id&lt;/code&gt;), create materialised views for dashboards (1000× cost drop typical), and choose on-demand vs flat-rate based on utilisation patterns.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does "rows removed by filter" mean in Postgres?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Rows Removed by Filter: N&lt;/code&gt; on a Postgres plan node means the executor &lt;strong&gt;read N rows and threw them away&lt;/strong&gt; — the filter predicate matched some fraction of the input, and the discarded rows are the loss. On a &lt;code&gt;Seq Scan&lt;/code&gt; with &lt;code&gt;Rows Removed by Filter: 99000000&lt;/code&gt;, the executor read 100 M rows and kept only 1 M — a scan that should have been an index seek. On an &lt;code&gt;Index Scan&lt;/code&gt; with non-zero &lt;code&gt;Rows Removed by Index Recheck&lt;/code&gt;, the index covered part of the predicate (say, the leading column) but a remaining condition (say, a trailing column not in the index) had to be re-checked at the heap; extending the index to a &lt;strong&gt;covering&lt;/strong&gt; &lt;code&gt;INCLUDE&lt;/code&gt; index eliminates the recheck. On a &lt;code&gt;Bitmap Heap Scan&lt;/code&gt; with &lt;code&gt;Rows Removed by Filter: N&lt;/code&gt;, the index bitmap was over-inclusive — the index isn't selective enough on its own; a partial index or a composite index that covers more of the predicate reduces the recheck. The general fix pattern is: any node with &lt;code&gt;Rows Removed by Filter&lt;/code&gt; in the millions is a &lt;strong&gt;missing index&lt;/strong&gt; or &lt;strong&gt;weak index&lt;/strong&gt; diagnosis — add or extend an index so the filter becomes an &lt;code&gt;Index Cond&lt;/code&gt; (part of the seek) instead of a &lt;code&gt;Filter&lt;/code&gt; (applied after the read).&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I fix a plan where the estimate is 10× off the actual?
&lt;/h3&gt;

&lt;p&gt;An estimate that's 10×+ off the actual usually means one of five things — &lt;strong&gt;stale statistics&lt;/strong&gt; (last &lt;code&gt;ANALYZE&lt;/code&gt; was days ago and data has shifted; run &lt;code&gt;ANALYZE table&lt;/code&gt; to refresh), &lt;strong&gt;skewed columns without extended stats&lt;/strong&gt; (a column with a highly-skewed distribution fools the histogram; create &lt;code&gt;CREATE STATISTICS s ON col FROM t;&lt;/code&gt; and run &lt;code&gt;ANALYZE&lt;/code&gt;), &lt;strong&gt;correlated columns treated as independent&lt;/strong&gt; (say, &lt;code&gt;city&lt;/code&gt; and &lt;code&gt;country&lt;/code&gt; are correlated but the planner multiplies their selectivities as if independent; &lt;code&gt;CREATE STATISTICS s (dependencies, ndistinct) ON city, country FROM t;&lt;/code&gt; fixes it), &lt;strong&gt;function-wrapped predicates&lt;/strong&gt; (&lt;code&gt;WHERE EXTRACT(YEAR FROM created_at) = 2026&lt;/code&gt; is not sargable; rewrite as a range &lt;code&gt;WHERE created_at BETWEEN '2026-01-01' AND '2027-01-01'&lt;/code&gt;), or &lt;strong&gt;plan cache mode mismatch&lt;/strong&gt; (a prepared statement was planned with generic parameters that don't match this call's actual values; force &lt;code&gt;plan_cache_mode = 'force_custom_plan'&lt;/code&gt; for this session). The fastest triage — &lt;strong&gt;always try &lt;code&gt;ANALYZE&lt;/code&gt; first&lt;/strong&gt; — it's free, fast, and fixes ~30% of estimate divergences on its own. If &lt;code&gt;ANALYZE&lt;/code&gt; doesn't help, look at the specific columns and predicates; extended statistics and predicate rewrites cover the remaining ~50%. As a last resort, force the plan via &lt;code&gt;pg_hint_plan&lt;/code&gt; or its equivalents on other engines — but hints lock the plan against future data shifts, so always add a comment explaining why and revisit periodically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practice on PipeCode
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Drill the &lt;a href="https://pipecode.ai/explore/practice/topic/optimization" rel="noopener noreferrer"&gt;SQL optimization practice library →&lt;/a&gt; for &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; reading, plan-tree diffs, &lt;code&gt;Rows Removed by Filter&lt;/code&gt; diagnostics, and one-line index or rewrite fixes.&lt;/li&gt;
&lt;li&gt;Sharpen the composite-index reflex with &lt;a href="https://pipecode.ai/explore/practice/topic/indexing" rel="noopener noreferrer"&gt;SQL indexing drills →&lt;/a&gt; — covering indexes, &lt;code&gt;INCLUDE&lt;/code&gt; columns, partial indexes, index-only scans, and cluster keys.&lt;/li&gt;
&lt;li&gt;Layer the join-optimisation surface with &lt;a href="https://pipecode.ai/explore/practice/topic/joins" rel="noopener noreferrer"&gt;SQL join drills →&lt;/a&gt; — hash vs merge vs nested loop, broadcast vs shuffle joins on warehouses, and skew mitigation.&lt;/li&gt;
&lt;li&gt;Push the SQL join-language depth with &lt;a href="https://pipecode.ai/explore/practice/topic/joins/sql" rel="noopener noreferrer"&gt;join drills on SQL →&lt;/a&gt; — semi-joins, anti-joins, lateral joins, and self-joins that show up in plan-reading interview questions.&lt;/li&gt;
&lt;li&gt;Warm up with &lt;a href="https://pipecode.ai/explore/practice/topic/aggregation" rel="noopener noreferrer"&gt;aggregation practice →&lt;/a&gt; — hash aggregate vs group aggregate, pre-aggregation patterns, and materialised-view design.&lt;/li&gt;
&lt;li&gt;Layer &lt;a href="https://pipecode.ai/explore/practice/topic/window-functions" rel="noopener noreferrer"&gt;window function drills →&lt;/a&gt; — &lt;code&gt;ROW_NUMBER()&lt;/code&gt; / &lt;code&gt;RANK()&lt;/code&gt; / &lt;code&gt;LAG()&lt;/code&gt; plans, partition-key alignment with clustering, and window-frame reading.&lt;/li&gt;
&lt;li&gt;Sharpen the general SQL surface with the &lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;SQL practice library →&lt;/a&gt; — 450+ DE-focused questions covering plan reading, index design, &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt;, and every adjacent pattern.&lt;/li&gt;
&lt;li&gt;For the broader SQL interview surface, take the &lt;a href="https://pipecode.ai/explore/courses/sql-for-data-engineering-interviews-from-zero-to-faang" rel="noopener noreferrer"&gt;SQL for Data Engineering course →&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/" rel="noopener noreferrer"&gt;Pipecode.ai&lt;/a&gt; is Leetcode for Data Engineering — every `sql explain analyze` recipe above ships with hands-on practice rooms where you type `EXPLAIN (ANALYZE, BUFFERS, VERBOSE)` on a live Postgres, read the plan tree bottom-up, spot the missing index from `Rows Removed by Filter`, migrate the same query to a Snowflake profile with pruning % and remote spill diagnosis, translate it into a BigQuery stage graph with `slot_ms` and `--dry_run` cost analysis, and finally read the SQL Server actual execution plan with `SET STATISTICS TIME/IO ON` — the exact four-engine `read execution plan` fluency that senior DE interviews probe. PipeCode pairs every plan-reading concept with 450+ DE-focused problems and a real-time scoring engine, so you never have to wonder whether your plan-diagnosis answer holds up under a senior interviewer's depth probes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/optimization" rel="noopener noreferrer"&gt;Practice plan reading now →&lt;/a&gt;&lt;br&gt;
&lt;a href="https://pipecode.ai/explore/practice/topic/indexing" rel="noopener noreferrer"&gt;Indexing drills →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>sql</category>
      <category>interview</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>SQL Pagination Done Right: OFFSET / FETCH / LIMIT / Keyset Cursors</title>
      <dc:creator>Gowtham Potureddi</dc:creator>
      <pubDate>Sat, 11 Jul 2026 16:22:36 +0000</pubDate>
      <link>https://dev.to/gowthampotureddi/sql-pagination-done-right-offset-fetch-limit-keyset-cursors-aj4</link>
      <guid>https://dev.to/gowthampotureddi/sql-pagination-done-right-offset-fetch-limit-keyset-cursors-aj4</guid>
      <description>&lt;p&gt;&lt;strong&gt;&lt;code&gt;sql pagination&lt;/code&gt;&lt;/strong&gt; is the single most-shipped, least-benchmarked primitive in every REST endpoint, GraphQL resolver, admin dashboard, and infinite-scroll feed a data engineer builds — and the single largest silent source of tail-latency incidents once the underlying table crosses 10 million rows. Every engineer types &lt;code&gt;LIMIT 20 OFFSET 40&lt;/code&gt; on their first week, ships it to production, and only discovers that &lt;code&gt;sql limit offset performance&lt;/code&gt; collapses at page 100,000 when the on-call graph turns red at 3 a.m. This guide is the honest, dialect-aware tour of what actually happens inside the planner when you ask for "page 47" of a 100-million-row table, why the answer looks fine at page 1 and terrifying at page 1,000,000, and what to reach for instead.&lt;/p&gt;

&lt;p&gt;The tour walks the four primitives you have to keep straight in 2026 — the classic &lt;code&gt;sql offset fetch&lt;/code&gt; family (Postgres / MySQL &lt;code&gt;LIMIT n OFFSET k&lt;/code&gt;, ANSI &lt;code&gt;OFFSET k ROWS FETCH FIRST n ROWS ONLY&lt;/code&gt;, SQL Server 2012+ &lt;code&gt;OFFSET k ROWS FETCH NEXT n ROWS ONLY&lt;/code&gt;, Oracle 12c+ &lt;code&gt;FETCH FIRST n ROWS ONLY&lt;/code&gt;, BigQuery and Snowflake &lt;code&gt;LIMIT n OFFSET k&lt;/code&gt;), the honest performance story of why OFFSET is &lt;code&gt;O(k + n)&lt;/code&gt; per page and why that curve goes vertical past a few hundred thousand rows, the &lt;code&gt;keyset pagination&lt;/code&gt; (a.k.a. &lt;code&gt;seek method pagination&lt;/code&gt;) pattern that replaces "skip k rows" with "give me everything after this cursor tuple" and produces the &lt;code&gt;O(log n + K)&lt;/code&gt; deep-page path that every modern feed on the internet actually uses, and full &lt;code&gt;sql cursor pagination&lt;/code&gt; for &lt;code&gt;sql infinite scroll&lt;/code&gt; feeds — opaque base64 tokens, HMAC signing, the GraphQL Relay connections spec, and the six-engine dialect matrix that tells you which keyword lands where. Every section ships a teaching block followed by a Solution-Tail interview answer — code, a step-by-step trace, an output table, then a concept-by-concept breakdown of why it works — so you leave with the two-line skeleton &lt;em&gt;and&lt;/em&gt; the reason it wins.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6qs7yrl5btatxei7rl8e.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6qs7yrl5btatxei7rl8e.jpeg" alt="PipeCode blog header for SQL pagination done right — bold white headline 'SQL PAGINATION' with subtitle 'OFFSET / FETCH / LIMIT vs Keyset Cursors' and a stylised scene showing a slow OFFSET scan-and-discard strip next to a fast keyset seek-arrow into a page window on a dark gradient with purple, green, orange, and blue accents and a small pipecode.ai attribution." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you want &lt;strong&gt;hands-on reps&lt;/strong&gt; immediately after reading, drill the &lt;a href="https://pipecode.ai/explore/practice/topic/pagination" rel="noopener noreferrer"&gt;SQL pagination practice library →&lt;/a&gt;, rehearse on &lt;a href="https://pipecode.ai/explore/practice/topic/order-by-limit" rel="noopener noreferrer"&gt;order-by-limit problems →&lt;/a&gt;, and sharpen the perf axis with the &lt;a href="https://pipecode.ai/explore/practice/topic/indexing" rel="noopener noreferrer"&gt;SQL indexing drill room →&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;On this page&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why pagination matters in 2026&lt;/li&gt;
&lt;li&gt;OFFSET / FETCH / LIMIT — dialect matrix&lt;/li&gt;
&lt;li&gt;Why OFFSET is slow at page 1M&lt;/li&gt;
&lt;li&gt;Keyset (seek method) pagination&lt;/li&gt;
&lt;li&gt;Cursor pagination + dialect matrix&lt;/li&gt;
&lt;li&gt;Cheat sheet — pagination recipe list&lt;/li&gt;
&lt;li&gt;Frequently asked questions&lt;/li&gt;
&lt;li&gt;Practice on PipeCode&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1. Why pagination matters in 2026
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The three shapes of &lt;code&gt;sql pagination&lt;/code&gt; — infinite scroll, page-numbered UIs, and CSV-style exports — and why the OFFSET default fails all three at scale
&lt;/h3&gt;

&lt;p&gt;The one-sentence invariant: &lt;strong&gt;&lt;code&gt;sql pagination&lt;/code&gt; is the primitive that lets a client request "the next N rows of this ordered result set without re-fetching the previous rows," and the choice between OFFSET-based and keyset-based pagination is the single decision that determines whether your endpoint tail latency stays flat as the underlying table grows from 10K rows to 10B rows&lt;/strong&gt;. Every backend eventually ships pagination; only a subset of them survive contact with a 10M-row table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where pagination actually shows up in production.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Infinite scroll feeds.&lt;/strong&gt; Twitter / X home timeline, Instagram feed, TikTok For You page, GitHub notifications, Slack conversation history. The client asks "give me 20 more" every time the user scrolls, forever. Random-access-to-page-N is not a requirement; strict ordering and stability under new inserts are.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Page-numbered admin dashboards.&lt;/strong&gt; "Show orders 1–50, page 2 is 51–100, ..." with a &lt;code&gt;« ‹ 1 2 3 ... 47 › »&lt;/code&gt; pager at the bottom. Random-access-to-page-47 &lt;em&gt;is&lt;/em&gt; a hard requirement; total row count usually is too. This is the shape where OFFSET is genuinely hard to replace.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bulk CSV / Parquet exports.&lt;/strong&gt; "Give me every row that matches this filter, split into 10K-row batches." Effectively a stream, not a paginator; users don't skip pages, they read from page 1 to the end.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API &lt;code&gt;list&lt;/code&gt; endpoints.&lt;/strong&gt; REST &lt;code&gt;GET /orders?limit=20&amp;amp;cursor=...&lt;/code&gt; or GraphQL &lt;code&gt;orders(first: 20, after: $endCursor)&lt;/code&gt;. Almost always keyset-based in 2026 — the Relay spec bakes cursors into the wire protocol.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data-warehouse dashboards.&lt;/strong&gt; Superset / Metabase / Looker "next page" buttons on aggregate tables. Aggregates are usually small enough that OFFSET works; row-level fact-table pagination in warehouses needs keyset with a clustering key.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The OFFSET trap in one sentence.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;LIMIT 20 OFFSET 1000000&lt;/code&gt; asks the planner to &lt;em&gt;read&lt;/em&gt; the first 1,000,020 rows in order and then &lt;em&gt;discard&lt;/em&gt; the first 1,000,000. There is no "skip ahead" — the engine walks the ordered scan row by row until it has counted past the offset, then emits the next 20. Cost is &lt;code&gt;O(k + n)&lt;/code&gt; per page. Across a full pagination from page 1 to page P, total cost is &lt;code&gt;O(P × (k + n))&lt;/code&gt; = &lt;code&gt;O(P² × n)&lt;/code&gt; on a fixed page size — quadratic in the number of pages fetched.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The "consistent read across pages" problem.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Between page 1 and page 2 of an OFFSET paginator, other transactions may have &lt;code&gt;INSERT&lt;/code&gt;ed rows above the current window or &lt;code&gt;DELETE&lt;/code&gt;d rows within it. The offset counter is &lt;em&gt;positional&lt;/em&gt;, not &lt;em&gt;keyed&lt;/em&gt;, so the same row can be seen twice or skipped entirely.&lt;/li&gt;
&lt;li&gt;Example — a feed sorted by &lt;code&gt;created_at DESC&lt;/code&gt;. Page 1 returns rows with timestamps &lt;code&gt;[10:05, 10:04, 10:03, ..., 09:46]&lt;/code&gt; (20 rows). Between the two calls a new row lands at &lt;code&gt;10:06&lt;/code&gt;. Page 2 with &lt;code&gt;OFFSET 20 LIMIT 20&lt;/code&gt; now starts at the &lt;em&gt;new&lt;/em&gt; row 21, which is the &lt;em&gt;old&lt;/em&gt; row 20 (&lt;code&gt;09:46&lt;/code&gt;) — the user sees &lt;code&gt;09:46&lt;/code&gt; twice.&lt;/li&gt;
&lt;li&gt;The dual — a row on page 1 gets deleted. Page 2 now starts one row earlier, skipping what was previously row 21. The user never sees that row.&lt;/li&gt;
&lt;li&gt;Both problems are latent in every OFFSET-based paginator against a mutating source. Keyset pagination cures them because it navigates by &lt;em&gt;key&lt;/em&gt; (the last row's tuple), not by &lt;em&gt;position&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why interviewers love pagination as a probe.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's a fluency check on plan reading.&lt;/strong&gt; Type &lt;code&gt;EXPLAIN SELECT * FROM events ORDER BY created_at DESC LIMIT 20 OFFSET 1000000&lt;/code&gt;, then describe the plan out loud. A senior candidate says "Seq Scan or Index Scan of the first 1,000,020 rows plus a Limit that emits the last 20" in one breath.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's a fluency check on index design.&lt;/strong&gt; "What index does this need?" — a candidate who says "&lt;code&gt;(created_at DESC, id DESC)&lt;/code&gt; for both OFFSET and keyset, but keyset is the one that actually uses it as a seek" signals senior instincts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's a fluency check on system design.&lt;/strong&gt; "Design the feed for a Twitter-scale product." Every good answer includes keyset cursors as the default and OFFSET as an anti-pattern.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's a fluency check on API design.&lt;/strong&gt; "Design a REST endpoint that returns paginated results." Every good answer includes an opaque cursor token, &lt;code&gt;hasNext&lt;/code&gt; / &lt;code&gt;hasPrev&lt;/code&gt;, and no &lt;code&gt;page=N&lt;/code&gt; query parameter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's a fluency check on failure modes.&lt;/strong&gt; "What happens if two users insert between your page 1 and page 2 calls?" A senior candidate walks through the double-read / skipped-row failure and shows how keyset avoids it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Six-engine keyword matrix — one-line summary.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Postgres 8.0+ / MySQL 4.0+ / Snowflake / BigQuery.&lt;/strong&gt; &lt;code&gt;LIMIT n OFFSET k&lt;/code&gt;. The de-facto default. Postgres also accepts the ANSI spelling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ANSI SQL:2008.&lt;/strong&gt; &lt;code&gt;OFFSET k ROWS FETCH FIRST n ROWS ONLY&lt;/code&gt;. Verbose, deterministic, standard. Postgres, Oracle 12c+, SQL Server 2012+, and Db2 all support it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL Server 2012+.&lt;/strong&gt; &lt;code&gt;OFFSET k ROWS FETCH NEXT n ROWS ONLY&lt;/code&gt;. The &lt;code&gt;NEXT&lt;/code&gt; is a synonym for &lt;code&gt;FIRST&lt;/code&gt;. Requires an &lt;code&gt;ORDER BY&lt;/code&gt; clause — the parser refuses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Oracle 12c+.&lt;/strong&gt; &lt;code&gt;OFFSET k ROWS FETCH FIRST n ROWS ONLY&lt;/code&gt;. Legacy Oracle (pre-12c) used the &lt;code&gt;ROWNUM&lt;/code&gt; pseudocolumn pattern.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All engines.&lt;/strong&gt; No dialect will save you from the &lt;code&gt;O(k + n)&lt;/code&gt; cost. OFFSET is a plan shape, not a keyword.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What senior interviewers actually probe.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Do you know the OFFSET cost model?&lt;/strong&gt; &lt;code&gt;O(k + n)&lt;/code&gt; per page — the planner scans and discards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do you know when OFFSET is fine?&lt;/strong&gt; For &lt;code&gt;k ≤ 1,000&lt;/code&gt; on a modestly wide table with a covering index — the scan is fast enough. Past &lt;code&gt;k ≥ 100,000&lt;/code&gt;, latency climbs into the "someone will file a ticket" range.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do you know the keyset skeleton?&lt;/strong&gt; &lt;code&gt;WHERE (order_col, id) &amp;lt; (:cursor_col, :cursor_id) ORDER BY order_col DESC, id DESC LIMIT n&lt;/code&gt;. One-shot tuple compare, one composite index seek.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do you know the tie-breaker rule?&lt;/strong&gt; Every paginated &lt;code&gt;ORDER BY&lt;/code&gt; needs a deterministic tie-breaker on a unique column (usually &lt;code&gt;id&lt;/code&gt;), or two rows with identical &lt;code&gt;created_at&lt;/code&gt; will alternate between pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do you know the NULLs rule?&lt;/strong&gt; &lt;code&gt;ORDER BY created_at DESC&lt;/code&gt; sorts NULLs first on some engines, last on others. Always spell it out — &lt;code&gt;ORDER BY created_at DESC NULLS LAST&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do you know the API-shape rule?&lt;/strong&gt; Cursors are opaque, base64-encoded, and HMAC-signed. Never expose the raw tuple to clients — it's a leaky abstraction and a tampering vector.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Worked example — the OFFSET query that ships to production on day one
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The archetype: given &lt;code&gt;events(id, user_id, created_at, payload)&lt;/code&gt;, return the first page of 20 most recent events. This is the query every backend intern types in week one. It is fine — for page 1. It becomes a tail-latency incident somewhere past page 10,000.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write a Postgres query that returns the &lt;em&gt;N&lt;/em&gt;-th page of 20 most recent events, given a page number &lt;em&gt;P&lt;/em&gt; (0-indexed). Then note the cost model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;created_at&lt;/th&gt;
&lt;th&gt;payload&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;2026-07-01 09:00&lt;/td&gt;
&lt;td&gt;login&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;2026-07-01 09:05&lt;/td&gt;
&lt;td&gt;click&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;2026-07-01 09:10&lt;/td&gt;
&lt;td&gt;login&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1000000&lt;/td&gt;
&lt;td&gt;91&lt;/td&gt;
&lt;td&gt;2026-07-10 08:59&lt;/td&gt;
&lt;td&gt;logout&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Page P of size 20, 0-indexed&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
&lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="p"&gt;(:&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;ORDER BY created_at DESC, id DESC&lt;/code&gt; — the compound sort key. The &lt;code&gt;id DESC&lt;/code&gt; tie-breaker ensures deterministic ordering when two events share a millisecond.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LIMIT 20&lt;/code&gt; — the page size. Clients typically pass &lt;code&gt;page_size&lt;/code&gt; as a query parameter capped at 100 or 500.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;OFFSET (:page * 20)&lt;/code&gt; — skip this many rows. On page 0, the offset is 0 and the query is fast. On page 50,000, the offset is 1,000,000 and the planner walks a million rows before emitting anything.&lt;/li&gt;
&lt;li&gt;The planner picks either a Seq Scan (if no covering index) or an Index Scan on &lt;code&gt;(created_at DESC, id DESC)&lt;/code&gt;. Either way, it walks the ordered stream one row at a time until it has counted past the offset, then emits the next 20.&lt;/li&gt;
&lt;li&gt;Cost model — &lt;code&gt;O(k + n)&lt;/code&gt; where &lt;code&gt;k&lt;/code&gt; is the offset and &lt;code&gt;n&lt;/code&gt; is the limit. Wall-clock latency grows linearly with &lt;code&gt;k&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output&lt;/strong&gt; (page 0, most recent 20 first).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;created_at&lt;/th&gt;
&lt;th&gt;payload&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1000000&lt;/td&gt;
&lt;td&gt;91&lt;/td&gt;
&lt;td&gt;2026-07-10 08:59&lt;/td&gt;
&lt;td&gt;logout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;999999&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;2026-07-10 08:58&lt;/td&gt;
&lt;td&gt;click&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; OFFSET is fine up to &lt;code&gt;k ≈ 1,000&lt;/code&gt; on a table with a covering index. Past that, latency becomes user-visible; past &lt;code&gt;k ≈ 100,000&lt;/code&gt;, latency crosses SLA. If your product needs a &lt;code&gt;« ‹ 1 2 ... 47 › »&lt;/code&gt; pager over a 10M+ row table, either cap the offset (Facebook's "5,000 pages" cap) or switch to keyset for the deep pages.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the OFFSET failure mode across concurrent writes
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A common source of pagination bugs — between page 1 and page 2, other transactions mutate the underlying table and the OFFSET counter drifts. The interviewer wants to hear "OFFSET is positional; keyset is keyed."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given the same events table, describe what happens if a user reads page 1 (offset 0, limit 20), then between calls someone inserts a new event with a &lt;code&gt;created_at&lt;/code&gt; newer than every existing row, then the user reads page 2 (offset 20, limit 20). Which rows does the user see?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pre-insert — 100 events with &lt;code&gt;created_at&lt;/code&gt; from &lt;code&gt;10:00:00&lt;/code&gt; (id=1) to &lt;code&gt;10:01:39&lt;/code&gt; (id=100). Sorted DESC, row 1 is &lt;code&gt;10:01:39&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Between calls — one new row lands with &lt;code&gt;created_at = 10:02:00&lt;/code&gt;, id=101.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Page 1 (returned to the user first)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Returns id=100 down to id=81&lt;/span&gt;

&lt;span class="c1"&gt;-- Concurrent insert lands: id=101, created_at=10:02:00&lt;/span&gt;

&lt;span class="c1"&gt;-- Page 2 (returned to the user second)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Returns id=81 down to id=62 — id=81 IS SEEN TWICE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Page 1 returns rows 100 → 81 (the twenty most recent). The user reads them.&lt;/li&gt;
&lt;li&gt;Between calls, one new row lands (id=101). The ordered stream is now &lt;code&gt;101, 100, 99, ..., 1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Page 2 asks for offset 20 limit 20 from the &lt;em&gt;new&lt;/em&gt; ordered stream. Offset 20 is now id=81; offset 39 is id=62.&lt;/li&gt;
&lt;li&gt;The user sees id=81 on both page 1 and page 2 — the duplicate — because the positional offset drifted by one when id=101 was inserted.&lt;/li&gt;
&lt;li&gt;Symmetrically, if id=90 had been &lt;em&gt;deleted&lt;/em&gt; between calls, page 2 would start at what was previously id=80 — the row at previous position 21 (id=80) would be &lt;em&gt;skipped&lt;/em&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt; Page 1 shows ids 100 → 81. Page 2 shows ids 81 → 62. id=81 appears in both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; OFFSET is positional and drifts under concurrent inserts / deletes. Keyset navigates by key (the last row's tuple), so a new row appearing above the cursor doesn't affect subsequent pages, and a deleted row inside a previous page doesn't cause a skip. Every social feed in the world uses keyset for this reason.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — engine dialect quickstart
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A quick sanity check — the six most common warehouses in 2026 and the pagination keyword each supports. Knowing this cold is a senior signal in system-design interviews.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given a list of engines, mark each with the OFFSET / LIMIT / FETCH keyword combination it supports as of 2026.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt; Postgres 16, MySQL 8, SQL Server 2022, Oracle 19c, Snowflake, BigQuery.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt; (No code — a dialect matrix answer.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Engine          | Preferred syntax                          | Notes
Postgres 9+     | LIMIT n OFFSET k (ANSI also supported)    | ORDER BY strongly recommended
MySQL 8         | LIMIT n OFFSET k or LIMIT k, n            | ORDER BY strongly recommended
SQL Server 2012+| OFFSET k ROWS FETCH NEXT n ROWS ONLY      | ORDER BY REQUIRED
Oracle 12c+     | OFFSET k ROWS FETCH FIRST n ROWS ONLY     | pre-12c uses ROWNUM
Snowflake       | LIMIT n OFFSET k                          | avoid OFFSET past 1M
BigQuery        | LIMIT n OFFSET k                          | OFFSET slow; consider partition prune
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Postgres has supported the &lt;code&gt;LIMIT n OFFSET k&lt;/code&gt; spelling since 8.0 and the ANSI &lt;code&gt;OFFSET k FETCH FIRST n ROWS ONLY&lt;/code&gt; since 8.4. Both compile to identical plans.&lt;/li&gt;
&lt;li&gt;MySQL supports &lt;code&gt;LIMIT n OFFSET k&lt;/code&gt; and the shorthand &lt;code&gt;LIMIT k, n&lt;/code&gt; (note the reversed operand order). Both compile to the same plan.&lt;/li&gt;
&lt;li&gt;SQL Server 2012 introduced OFFSET / FETCH and &lt;em&gt;requires&lt;/em&gt; an &lt;code&gt;ORDER BY&lt;/code&gt; clause — the parser refuses without one, unlike Postgres and MySQL which allow (but discourage) unordered pagination.&lt;/li&gt;
&lt;li&gt;Oracle 12c (2013) added ANSI OFFSET / FETCH. Older Oracle used a &lt;code&gt;ROWNUM&lt;/code&gt; filter — &lt;code&gt;WHERE ROWNUM &amp;lt;= 100 MINUS WHERE ROWNUM &amp;lt;= 80&lt;/code&gt; — messy but the only pre-12c option.&lt;/li&gt;
&lt;li&gt;Snowflake and BigQuery both accept &lt;code&gt;LIMIT n OFFSET k&lt;/code&gt; but neither warehouse is optimised for deep OFFSET — micro-partitions on Snowflake or partition-and-cluster pruning on BigQuery mean the scan-and-discard cost is amplified by columnar-read overhead.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt; The matrix above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Memorise the six-row list. In an interview, if asked "how do you paginate on X," the first sentence should be the keyword combination and the second should be "with ORDER BY plus a deterministic tie-breaker on id."&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on when to use OFFSET vs keyset
&lt;/h3&gt;

&lt;p&gt;A senior interviewer often opens with: "You're designing the paginated &lt;code&gt;/api/v1/orders&lt;/code&gt; endpoint for a two-billion-row orders table. The product wants both an infinite-scroll consumer app and a page-numbered admin dashboard with &lt;code&gt;« ‹ 1 2 ... 100000 › »&lt;/code&gt;. Which pagination strategy do you pick for each, and why?"&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using dual-endpoint design — keyset for infinite-scroll, capped OFFSET for admin
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Consumer app — infinite scroll, keyset-only, opaque cursors
# GET /api/v1/orders?first=20&amp;amp;after=&amp;lt;cursor&amp;gt;
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;list_orders_keyset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;after_cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Cap first at 100 to protect the DB
&lt;/span&gt;    &lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;after_cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;oid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decode_and_verify_hmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;after_cursor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            SELECT id, created_at, user_id, total
            FROM orders
            WHERE (created_at, id) &amp;lt; (%s, %s)
            ORDER BY created_at DESC, id DESC
            LIMIT %s + 1
            &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;oid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            SELECT id, created_at, user_id, total
            FROM orders
            ORDER BY created_at DESC, id DESC
            LIMIT %s + 1
            &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;,),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;has_next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;end_cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;encode_hmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;edges&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;node&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cursor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;encode_hmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pageInfo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hasNext&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;has_next&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;endCursor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;end_cursor&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="c1"&gt;# Admin dashboard — page-numbered, capped OFFSET, total_count
# GET /admin/orders?page=&amp;lt;int&amp;gt;&amp;amp;page_size=&amp;lt;int&amp;gt;
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;list_orders_admin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page_size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Cap page at 5000, page_size at 200
&lt;/span&gt;    &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;5_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;page_size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;total_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query_scalar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT COUNT(*) FROM orders&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        SELECT id, created_at, user_id, total
        FROM orders
        ORDER BY created_at DESC, id DESC
        LIMIT %s OFFSET %s
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;page_size&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rows&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;page&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;page_size&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;page_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;total_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total_pages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_count&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;page_size&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;page_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Consumer app (keyset)&lt;/th&gt;
&lt;th&gt;Admin dashboard (OFFSET)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;client calls with &lt;code&gt;after=null&lt;/code&gt;, &lt;code&gt;first=20&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;client calls with &lt;code&gt;page=0&lt;/code&gt;, &lt;code&gt;page_size=50&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;first=capped at 100; no cursor&lt;/td&gt;
&lt;td&gt;page capped at 5000; page_size at 200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;SELECT with LIMIT 21 (first+1)&lt;/td&gt;
&lt;td&gt;SELECT with LIMIT 50 OFFSET 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;21 rows returned; hasNext=true&lt;/td&gt;
&lt;td&gt;50 rows returned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;rows[:20] emitted; endCursor from row 20&lt;/td&gt;
&lt;td&gt;rows emitted with total_count and total_pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;next call passes endCursor as after=&lt;/td&gt;
&lt;td&gt;next call passes page=1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;O(log n + 20) per page, flat forever&lt;/td&gt;
&lt;td&gt;O(k + 50), fine for page ≤ 5000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The consumer app uses keyset because the infinite-scroll UX has no page-number requirement and needs constant-time deep pagination. The admin dashboard uses OFFSET because "jump to page 47" is a real product requirement, but caps the maximum page at 5,000 (like Facebook's search-result cap) so no admin can accidentally trigger a 100-million-row scan.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Cost model&lt;/th&gt;
&lt;th&gt;Deep-page behaviour&lt;/th&gt;
&lt;th&gt;Stability under writes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Consumer (keyset)&lt;/td&gt;
&lt;td&gt;O(log n + K)&lt;/td&gt;
&lt;td&gt;flat 5–10 ms at any depth&lt;/td&gt;
&lt;td&gt;perfect — no drift&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Admin (capped OFFSET)&lt;/td&gt;
&lt;td&gt;O(k + n), k ≤ 5000×200=1M&lt;/td&gt;
&lt;td&gt;slow-but-bounded past 100K rows&lt;/td&gt;
&lt;td&gt;positional — mild drift&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Dual-endpoint design matches the two consumer shapes&lt;/strong&gt;&lt;/strong&gt; — the consumer feed never needs "page 47"; it needs "the next 20 after this cursor forever." Keyset gives that with &lt;code&gt;O(log n + K)&lt;/code&gt; deep pages. The admin dashboard &lt;em&gt;does&lt;/em&gt; need "page 47"; capped OFFSET gives that with a bounded worst case.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;&lt;code&gt;LIMIT first + 1&lt;/code&gt; is the peek-ahead trick&lt;/strong&gt;&lt;/strong&gt; — the endpoint fetches 21 rows but returns 20; the 21st row's existence tells the client whether &lt;code&gt;hasNext&lt;/code&gt; is true. Zero extra queries, exact answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;HMAC-signed opaque cursors&lt;/strong&gt;&lt;/strong&gt; — the cursor &lt;code&gt;after&lt;/code&gt; parameter is a base64-encoded &lt;code&gt;(created_at, id)&lt;/code&gt; tuple with an HMAC signature. Clients can't tamper with the tuple to leak rows they shouldn't see; servers can validate the signature in constant time before decoding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Capped OFFSET protects the DB&lt;/strong&gt;&lt;/strong&gt; — a page-number cap of 5,000 with a page-size cap of 200 means the absolute worst case is &lt;code&gt;OFFSET 999,800 LIMIT 200&lt;/code&gt; — a ~1M-row scan, painful but bounded. Without the cap, an admin could drop &lt;code&gt;page=1000000&lt;/code&gt; and hang the DB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — Consumer endpoint is &lt;code&gt;O(log n + K)&lt;/code&gt; per page where &lt;code&gt;n&lt;/code&gt; is orders.rowcount and &lt;code&gt;K = first&lt;/code&gt;. Admin endpoint is &lt;code&gt;O(k + n)&lt;/code&gt; per page where &lt;code&gt;k&lt;/code&gt; is &lt;code&gt;page × page_size&lt;/code&gt; and &lt;code&gt;n = page_size&lt;/code&gt;. On a 2B-row table with a covering &lt;code&gt;(created_at DESC, id DESC)&lt;/code&gt; index, the consumer endpoint runs at ~8 ms per page at any depth; the admin endpoint runs at ~5 ms at page 0, ~50 ms at page 100, ~500 ms at page 10K, and ~5 s at page 100K (which the cap prevents from exceeding).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — pagination&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL pagination problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/pagination" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — order by / limit&lt;/span&gt;
&lt;strong&gt;ORDER BY + LIMIT drills&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/order-by-limit" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  2. OFFSET / FETCH / LIMIT — dialect matrix
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;sql offset fetch&lt;/code&gt; and &lt;code&gt;LIMIT&lt;/code&gt; across Postgres, MySQL, SQL Server, Oracle, Snowflake, and BigQuery — same primitive, six spellings, one shared perf model
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;every engine implements pagination with two knobs — a skip count (&lt;code&gt;OFFSET k&lt;/code&gt; or &lt;code&gt;LIMIT k, n&lt;/code&gt;'s first operand) and a take count (&lt;code&gt;LIMIT n&lt;/code&gt; or &lt;code&gt;FETCH FIRST n ROWS ONLY&lt;/code&gt;) — and the choice of keyword changes nothing about the underlying plan shape, which is always "scan the ordered stream, discard k, emit n"&lt;/strong&gt;. Once you memorise the six spellings, translating a pagination query across dialects is mechanical.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvf1b06rpzjx8zvwkwf7o.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvf1b06rpzjx8zvwkwf7o.jpeg" alt="Visual diagram of the OFFSET / FETCH / LIMIT dialect matrix across Postgres, MySQL, SQL Server, Oracle, Snowflake, and BigQuery — six columns of syntax cards with each engine's keyword highlighted plus an ORDER BY requirement banner and a ties/tiebreaker annotation strip; on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Slot 1 — Postgres and MySQL (the &lt;code&gt;LIMIT n OFFSET k&lt;/code&gt; family).&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Postgres — &lt;code&gt;SELECT ... ORDER BY ... LIMIT n OFFSET k&lt;/code&gt;. Also accepts the ANSI &lt;code&gt;OFFSET k ROWS FETCH FIRST n ROWS ONLY&lt;/code&gt; since 8.4.&lt;/li&gt;
&lt;li&gt;MySQL 8 — &lt;code&gt;SELECT ... ORDER BY ... LIMIT n OFFSET k&lt;/code&gt; or the shorthand &lt;code&gt;LIMIT k, n&lt;/code&gt; (note the reversed operand order — offset then limit). Both compile identically.&lt;/li&gt;
&lt;li&gt;Snowflake — &lt;code&gt;LIMIT n OFFSET k&lt;/code&gt;. Micro-partition pruning helps at page 1 but not at deep offsets.&lt;/li&gt;
&lt;li&gt;BigQuery — &lt;code&gt;LIMIT n OFFSET k&lt;/code&gt;. Documented as slow on deep offsets; the Google docs explicitly recommend keyset pagination past a few thousand rows.&lt;/li&gt;
&lt;li&gt;Redshift, ClickHouse, DuckDB, SQLite — all support &lt;code&gt;LIMIT n OFFSET k&lt;/code&gt; with the same semantics.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 2 — the ANSI SQL:2008 spelling.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;OFFSET k ROWS FETCH FIRST n ROWS ONLY&lt;/code&gt; — verbose, standard, deterministic. Every "ANSI-compliant" dialect accepts it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;OFFSET k ROWS FETCH NEXT n ROWS ONLY&lt;/code&gt; — the SQL-Server-idiomatic spelling; &lt;code&gt;NEXT&lt;/code&gt; is a synonym for &lt;code&gt;FIRST&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FETCH FIRST n ROWS ONLY&lt;/code&gt; — without OFFSET, equivalent to &lt;code&gt;LIMIT n&lt;/code&gt;. Common on Oracle where &lt;code&gt;LIMIT&lt;/code&gt; is not a keyword.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FETCH FIRST n ROWS WITH TIES&lt;/code&gt; — a variant that returns extra rows past the limit if they tie on the ORDER BY key. Rarely used in pagination (breaks page-size invariants) but the interviewer might ask about it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 3 — SQL Server-specific rules.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;OFFSET k ROWS FETCH NEXT n ROWS ONLY&lt;/code&gt; — the SQL Server 2012+ syntax. &lt;strong&gt;Requires an &lt;code&gt;ORDER BY&lt;/code&gt; clause&lt;/strong&gt; — the parser refuses without one, unlike Postgres and MySQL.&lt;/li&gt;
&lt;li&gt;Pre-2012 SQL Server — used &lt;code&gt;ROW_NUMBER() OVER (ORDER BY ...) AS rn&lt;/code&gt; in a CTE and &lt;code&gt;WHERE rn BETWEEN k+1 AND k+n&lt;/code&gt; to paginate. Ugly, but the only option.&lt;/li&gt;
&lt;li&gt;SQL Server also supports &lt;code&gt;TOP n&lt;/code&gt; for the first-page-only case (&lt;code&gt;SELECT TOP 20 ...&lt;/code&gt;), which is not pagination — it just limits the result set without offset support.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 4 — Oracle-specific rules.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Oracle 12c+ — &lt;code&gt;OFFSET k ROWS FETCH FIRST n ROWS ONLY&lt;/code&gt;. Fully ANSI-compliant, same semantics as SQL Server 2012+.&lt;/li&gt;
&lt;li&gt;Pre-12c Oracle (11g and earlier) — used the &lt;code&gt;ROWNUM&lt;/code&gt; pseudocolumn: &lt;code&gt;SELECT * FROM (SELECT rownum AS rn, t.* FROM t ORDER BY ... ) WHERE rn BETWEEN k+1 AND k+n&lt;/code&gt;. Legacy but still shows up in enterprise codebases.&lt;/li&gt;
&lt;li&gt;Oracle 21c added &lt;code&gt;FETCH FIRST n PERCENT ROWS ONLY&lt;/code&gt; — for percent-based limits — but this isn't a pagination primitive per se.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 5 — BigQuery and Snowflake specifics.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;BigQuery — &lt;code&gt;LIMIT n OFFSET k&lt;/code&gt;, but OFFSET is documented as an anti-pattern past a few thousand rows. The recommended replacement is keyset. BigQuery also has &lt;code&gt;LIMIT n&lt;/code&gt; with an implicit &lt;code&gt;OFFSET 0&lt;/code&gt; — same as everywhere.&lt;/li&gt;
&lt;li&gt;Snowflake — &lt;code&gt;LIMIT n OFFSET k&lt;/code&gt;. Snowflake's micro-partition metadata helps prune the initial scan but still has to walk the ordered result set past the offset. Deep OFFSET on a 1B-row Snowflake table is measurably slow.&lt;/li&gt;
&lt;li&gt;Databricks SQL — &lt;code&gt;LIMIT n OFFSET k&lt;/code&gt; since 2022. Same behaviour as Snowflake.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 6 — the ORDER BY requirement.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Without &lt;code&gt;ORDER BY&lt;/code&gt;, pagination is nondeterministic. Two calls with the same &lt;code&gt;LIMIT / OFFSET&lt;/code&gt; may return different rows because the underlying scan order is not guaranteed stable.&lt;/li&gt;
&lt;li&gt;SQL Server &lt;em&gt;requires&lt;/em&gt; &lt;code&gt;ORDER BY&lt;/code&gt; for OFFSET / FETCH — the parser rejects the query.&lt;/li&gt;
&lt;li&gt;Postgres, MySQL, Snowflake, BigQuery &lt;em&gt;allow&lt;/em&gt; omitting &lt;code&gt;ORDER BY&lt;/code&gt; but will return unstable results. Every senior review comment says "add ORDER BY plus a tie-breaker on id."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Composite ORDER BY&lt;/strong&gt; — the sort key must be a superset of the columns you want to compare in a cursor. For a "most recent" feed, the sort is &lt;code&gt;ORDER BY created_at DESC, id DESC&lt;/code&gt; — &lt;code&gt;id&lt;/code&gt; as tie-breaker.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 7 — ties and tie-breakers.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If two rows share the same &lt;code&gt;created_at&lt;/code&gt; (millisecond precision or worse), plain &lt;code&gt;ORDER BY created_at DESC LIMIT 20 OFFSET 20&lt;/code&gt; may put one on page 1 and the other on page 2 — or vice versa, on the next call. The results are unstable.&lt;/li&gt;
&lt;li&gt;Fix — always include a unique tie-breaker: &lt;code&gt;ORDER BY created_at DESC, id DESC&lt;/code&gt;. Every pagination code review comment worth reading says this.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FETCH FIRST n ROWS WITH TIES&lt;/code&gt; — the ANSI variant that returns &lt;em&gt;all&lt;/em&gt; rows that tie on the sort key at the limit boundary. Rarely appropriate for pagination because it violates the "exactly n rows per page" invariant.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 8 — NULLs ordering.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ORDER BY created_at DESC&lt;/code&gt; — where do NULLs go? Postgres and Oracle put NULLs first; MySQL and SQL Server put NULLs last; BigQuery puts NULLs first.&lt;/li&gt;
&lt;li&gt;Fix — spell it out: &lt;code&gt;ORDER BY created_at DESC NULLS LAST&lt;/code&gt;. Supported on Postgres, Oracle, and BigQuery. MySQL 8 supports the syntax but the behaviour matches the engine default anyway.&lt;/li&gt;
&lt;li&gt;SQL Server does not support &lt;code&gt;NULLS FIRST / LAST&lt;/code&gt; syntax — the workaround is &lt;code&gt;ORDER BY (CASE WHEN created_at IS NULL THEN 1 ELSE 0 END), created_at DESC&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common newbie mistakes.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Forgetting &lt;code&gt;ORDER BY&lt;/code&gt; — the query "works" on page 1 but returns different rows on subsequent pages because the plan changed.&lt;/li&gt;
&lt;li&gt;Omitting a unique tie-breaker — two rows with the same &lt;code&gt;created_at&lt;/code&gt; can appear on both page 1 and page 2 or on neither.&lt;/li&gt;
&lt;li&gt;Using &lt;code&gt;SELECT *&lt;/code&gt; — projecting more columns than needed forces a heap fetch that OFFSET amplifies. Always project the minimum column set.&lt;/li&gt;
&lt;li&gt;Assuming OFFSET is &lt;code&gt;O(1)&lt;/code&gt; — it's &lt;code&gt;O(k + n)&lt;/code&gt;. On page 1M, that's a million rows scanned before the first byte is returned.&lt;/li&gt;
&lt;li&gt;Assuming pagination is stable under writes — OFFSET is positional and drifts. Every social feed uses keyset.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How OFFSET interacts with EXPLAIN.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;EXPLAIN SELECT * FROM events ORDER BY created_at DESC LIMIT 20 OFFSET 1000000&lt;/code&gt; on Postgres — the plan shows &lt;code&gt;Limit → Index Scan Backward using idx_events_created_at&lt;/code&gt;. The &lt;code&gt;Limit&lt;/code&gt; step has &lt;code&gt;actual rows=20&lt;/code&gt; but &lt;code&gt;actual loops = 1,000,020&lt;/code&gt; — the engine walks a million rows before hitting the limit.&lt;/li&gt;
&lt;li&gt;The same query with keyset — &lt;code&gt;WHERE (created_at, id) &amp;lt; (:ts, :id) ORDER BY created_at DESC, id DESC LIMIT 20&lt;/code&gt; — shows &lt;code&gt;Limit → Index Scan Backward&lt;/code&gt; with &lt;code&gt;actual rows=20 actual loops=20&lt;/code&gt;. One seek, twenty reads, done.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;Buffers: shared read=100000&lt;/code&gt; line in &lt;code&gt;EXPLAIN (ANALYZE, BUFFERS)&lt;/code&gt; is the smoking gun — OFFSET buffers grow linearly with &lt;code&gt;k&lt;/code&gt;; keyset buffers stay constant.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Interview probes on syntax fluency.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Which dialect requires ORDER BY for OFFSET/FETCH?"&lt;/strong&gt; — SQL Server 2012+. Postgres, MySQL, Oracle, Snowflake, BigQuery all allow (but discourage) omitting it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"What is the shorthand &lt;code&gt;LIMIT 40, 20&lt;/code&gt; in MySQL?"&lt;/strong&gt; — offset 40, limit 20. Note the reversed operand order (offset first, then limit) — the most confused MySQL trivia question in interviews.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"How do you paginate on Oracle 11g?"&lt;/strong&gt; — the legacy &lt;code&gt;ROWNUM&lt;/code&gt; filter inside a subquery. Not the ANSI FETCH FIRST because that came in 12c.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"How do you handle NULLs on SQL Server's ORDER BY?"&lt;/strong&gt; — the &lt;code&gt;CASE WHEN … IS NULL&lt;/code&gt; trick, because SQL Server doesn't support &lt;code&gt;NULLS FIRST / LAST&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — Postgres, MySQL, and SQL Server pagination side by side
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The clearest way to see the dialect mechanics: write the same "20 most recent orders, page 47" query in three dialects. Once you can round-trip the syntax, the mental model locks in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given a table &lt;code&gt;orders(id, user_id, created_at, total)&lt;/code&gt;, write the page-47-of-20 query in Postgres, MySQL 8, and SQL Server 2022. Note any syntactic requirements each engine imposes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;created_at&lt;/th&gt;
&lt;th&gt;total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;2026-07-10 09:00&lt;/td&gt;
&lt;td&gt;25.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;td&gt;2026-07-10 09:01&lt;/td&gt;
&lt;td&gt;40.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Postgres&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="mi"&gt;940&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- MySQL 8 (identical to Postgres; also accepts LIMIT 940, 20 shorthand)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="mi"&gt;940&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- SQL Server 2022 — ORDER BY is REQUIRED&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="mi"&gt;940&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt;
&lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="k"&gt;NEXT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Postgres uses &lt;code&gt;LIMIT 20 OFFSET 940&lt;/code&gt;. The order is "take 20, skip 940." Postgres also supports the ANSI spelling — &lt;code&gt;OFFSET 940 ROWS FETCH FIRST 20 ROWS ONLY&lt;/code&gt; — and treats it identically.&lt;/li&gt;
&lt;li&gt;MySQL 8 supports the identical &lt;code&gt;LIMIT 20 OFFSET 940&lt;/code&gt; plus the classic shorthand &lt;code&gt;LIMIT 940, 20&lt;/code&gt; (note the operand order — offset first). The shorthand is the one 90% of MySQL codebases still use; the explicit form is more readable.&lt;/li&gt;
&lt;li&gt;SQL Server 2022 uses &lt;code&gt;OFFSET 940 ROWS FETCH NEXT 20 ROWS ONLY&lt;/code&gt;. The &lt;code&gt;NEXT&lt;/code&gt; is a synonym for &lt;code&gt;FIRST&lt;/code&gt;. The &lt;code&gt;ROWS&lt;/code&gt; keyword is required. &lt;code&gt;ORDER BY&lt;/code&gt; is &lt;em&gt;required&lt;/em&gt; — the parser refuses without one, giving a clear compile-time error.&lt;/li&gt;
&lt;li&gt;All three plans compile to the same shape — walk the sorted stream past 940 rows, emit the next 20. Actual latency is nearly identical on all three engines for the same page depth.&lt;/li&gt;
&lt;li&gt;Migrating pagination between the three is mechanical — swap &lt;code&gt;LIMIT n OFFSET k&lt;/code&gt; for &lt;code&gt;OFFSET k ROWS FETCH NEXT n ROWS ONLY&lt;/code&gt; (and vice versa).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output&lt;/strong&gt; (page 47 = rows 941–960).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;created_at&lt;/th&gt;
&lt;th&gt;total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;999059&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;2026-07-10 08:15&lt;/td&gt;
&lt;td&gt;30.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Learn both spellings. Every migration from a MySQL app to a SQL Server backend or from Postgres to Oracle rewrites pagination. The mechanical swap is a five-second edit; forgetting it is a compile error.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — Oracle 11g's &lt;code&gt;ROWNUM&lt;/code&gt; pattern vs Oracle 12c+'s &lt;code&gt;FETCH FIRST&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Legacy Oracle codebases still use the &lt;code&gt;ROWNUM&lt;/code&gt; pseudocolumn for pagination. Interviewers love this — it's a fluency check on both "you know the old syntax" and "you know the new one."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the "page 47 of 20 orders" query on Oracle 11g (legacy) and Oracle 19c (modern). Explain why the legacy pattern needs a subquery.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt; (Same as above.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Oracle 11g and earlier — ROWNUM inside a subquery&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;rownum&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
    &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;rownum&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;960&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;940&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Oracle 12c+ — ANSI FETCH FIRST&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="mi"&gt;940&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt;
&lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Oracle's &lt;code&gt;ROWNUM&lt;/code&gt; is assigned &lt;strong&gt;after&lt;/strong&gt; the WHERE clause but &lt;strong&gt;before&lt;/strong&gt; the ORDER BY. The naive &lt;code&gt;SELECT ROWNUM, ... FROM t ORDER BY x&lt;/code&gt; returns numbered-then-sorted rows, not sorted-then-numbered — the numbering is on the wrong data.&lt;/li&gt;
&lt;li&gt;The workaround — a nested subquery. The inner &lt;code&gt;SELECT&lt;/code&gt; orders the rows; the middle &lt;code&gt;SELECT&lt;/code&gt; numbers them with &lt;code&gt;ROWNUM&lt;/code&gt;; the outer &lt;code&gt;SELECT&lt;/code&gt; filters by the numbered range.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;WHERE rownum &amp;lt;= 960&lt;/code&gt; on the middle query is a common perf trick — Oracle can short-circuit once it has assigned 960 numbers, avoiding a full sort of the underlying table.&lt;/li&gt;
&lt;li&gt;Oracle 12c (2013) shipped ANSI OFFSET / FETCH, so modern code should always prefer the second form. It's more readable, ships with the same plan shape, and is portable across engines.&lt;/li&gt;
&lt;li&gt;If you inherit legacy Oracle code, recognise the three-level nested pattern instantly. It is one of the top-three most-migrated SQL constructs in Oracle-to-Postgres modernisation projects.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt; Both queries return the same page 47.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; On Oracle 12c+, always use &lt;code&gt;OFFSET k ROWS FETCH FIRST n ROWS ONLY&lt;/code&gt;. On Oracle 11g and earlier, use the three-level &lt;code&gt;ROWNUM&lt;/code&gt; subquery. Migrating from 11g to 12c is a mechanical rewrite; interview candidates who can spot both patterns and translate between them signal senior instincts.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — BigQuery's OFFSET warning and the recommended keyset alternative
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; BigQuery documents OFFSET as an anti-pattern past a few thousand rows. The recommended replacement is keyset — and the interviewer might ask why BigQuery is more sensitive to deep OFFSET than a row-store database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given a 10B-row BigQuery &lt;code&gt;events&lt;/code&gt; table partitioned by DATE(created_at), why is &lt;code&gt;LIMIT 20 OFFSET 1000000&lt;/code&gt; an anti-pattern? What's the recommended alternative?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt; (Same events table as before, at 10B rows.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- BigQuery — anti-pattern past ~10K OFFSET&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;`project.dataset.events`&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="s1"&gt;'2026-07-01'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="s1"&gt;'2026-07-10'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="mi"&gt;1000000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- BigQuery — recommended keyset&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;`project.dataset.events`&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="s1"&gt;'2026-07-01'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="s1"&gt;'2026-07-10'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;cursor_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;cursor_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;BigQuery is a columnar warehouse — each column is stored separately and reads are page-oriented. Skipping a million rows means reading a million-row-worth of column data across every projected column, then discarding it.&lt;/li&gt;
&lt;li&gt;Even with partition pruning on the DATE(created_at) filter, the OFFSET step happens &lt;em&gt;after&lt;/em&gt; the scan — the pruning helps but does not eliminate the scan-and-discard cost.&lt;/li&gt;
&lt;li&gt;The keyset alternative uses &lt;code&gt;WHERE (created_at, id) &amp;lt; (@cursor_ts, @cursor_id)&lt;/code&gt; — a range predicate that BigQuery can push into partition and cluster pruning. If the table is clustered on &lt;code&gt;created_at&lt;/code&gt;, the query touches only the blocks that contain the target page.&lt;/li&gt;
&lt;li&gt;On a 10B-row events table with DATE partitioning and &lt;code&gt;created_at&lt;/code&gt; clustering, &lt;code&gt;OFFSET 1000000&lt;/code&gt; runs in ~30 seconds; the keyset variant runs in ~200 ms — a 150× win.&lt;/li&gt;
&lt;li&gt;Every BigQuery pagination guide since 2019 has recommended keyset for anything past a few thousand rows. The Snowflake guidance is identical (micro-partitions instead of BigQuery partitions, but the same shape).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query&lt;/th&gt;
&lt;th&gt;Rows scanned&lt;/th&gt;
&lt;th&gt;Slot time&lt;/th&gt;
&lt;th&gt;Wall-clock&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OFFSET 1000000&lt;/td&gt;
&lt;td&gt;1,000,020&lt;/td&gt;
&lt;td&gt;30 s slot-seconds&lt;/td&gt;
&lt;td&gt;~30 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keyset&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;0.02 s slot-seconds&lt;/td&gt;
&lt;td&gt;~200 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; In a columnar warehouse (BigQuery, Snowflake, Redshift), OFFSET's cost is amplified by the columnar-read overhead — projecting &lt;em&gt;fewer&lt;/em&gt; columns helps a little, but the fundamental scan-and-discard cost stays. Keyset is the only stable long-form pagination on warehouses.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on cross-warehouse pagination portability
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Design a paginated &lt;code&gt;orders&lt;/code&gt; endpoint that runs identically on Postgres (production OLTP), Snowflake (analytics), and BigQuery (data-warehouse). Where can you use OFFSET, where must you use keyset, and how do you keep the API contract stable across all three?"&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using dialect-dispatched dbt macro with a shared cursor contract
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- macros/paginate_orders.sql (dbt)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;macro&lt;/span&gt; &lt;span class="n"&gt;paginate_orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cursor_ts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cursor_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%-&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'postgres'&lt;/span&gt; &lt;span class="o"&gt;-%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;-- OLTP: keyset with covering index (created_at DESC, id DESC)&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cursor_ts&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;none&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;({{&lt;/span&gt; &lt;span class="n"&gt;cursor_ts&lt;/span&gt; &lt;span class="p"&gt;}},&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;cursor_id&lt;/span&gt; &lt;span class="p"&gt;}})&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;true&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;endif&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
    &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;page_size&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%-&lt;/span&gt; &lt;span class="n"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'snowflake'&lt;/span&gt; &lt;span class="o"&gt;-%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;-- Analytics: keyset with clustering key on created_at&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cursor_ts&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;none&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;({{&lt;/span&gt; &lt;span class="n"&gt;cursor_ts&lt;/span&gt; &lt;span class="p"&gt;}},&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;cursor_id&lt;/span&gt; &lt;span class="p"&gt;}})&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;TRUE&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;endif&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
    &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;page_size&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%-&lt;/span&gt; &lt;span class="n"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'bigquery'&lt;/span&gt; &lt;span class="o"&gt;-%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;-- DW: keyset + partition pruning on DATE(created_at)&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;`{{ project }}.orders`&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt;
      &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt;
        &lt;span class="n"&gt;DATE_SUB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CURRENT_DATE&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;CURRENT_DATE&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cursor_ts&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;none&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;({{&lt;/span&gt; &lt;span class="n"&gt;cursor_ts&lt;/span&gt; &lt;span class="p"&gt;}},&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;cursor_id&lt;/span&gt; &lt;span class="p"&gt;}})&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;endif&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
    &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;page_size&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%-&lt;/span&gt; &lt;span class="n"&gt;endif&lt;/span&gt; &lt;span class="o"&gt;-%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;endmacro&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dispatch branch&lt;/th&gt;
&lt;th&gt;What runs&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Postgres&lt;/td&gt;
&lt;td&gt;keyset with &lt;code&gt;(created_at, id)&lt;/code&gt; covering btree&lt;/td&gt;
&lt;td&gt;OLTP; index seek is O(log n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snowflake&lt;/td&gt;
&lt;td&gt;keyset with &lt;code&gt;created_at&lt;/code&gt; clustering key&lt;/td&gt;
&lt;td&gt;analytics; micro-partition prune&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BigQuery&lt;/td&gt;
&lt;td&gt;keyset + partition prune on DATE(created_at)&lt;/td&gt;
&lt;td&gt;warehouse; partition + cluster prune&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;All&lt;/td&gt;
&lt;td&gt;LIMIT page_size + 1 for hasNext peek-ahead&lt;/td&gt;
&lt;td&gt;uniform API contract&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;All&lt;/td&gt;
&lt;td&gt;ORDER BY created_at DESC, id DESC&lt;/td&gt;
&lt;td&gt;deterministic ordering&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The macro dispatches on &lt;code&gt;target.type&lt;/code&gt;. Postgres, Snowflake, and BigQuery each get a keyset variant optimised for that engine's storage model (btree, clustering, partitioning). The output schema and cursor contract are identical — every caller sees &lt;code&gt;(id, user_id, created_at, total)&lt;/code&gt; plus a peek-ahead row to compute &lt;code&gt;hasNext&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;target&lt;/th&gt;
&lt;th&gt;page cost&lt;/th&gt;
&lt;th&gt;deep-page cost&lt;/th&gt;
&lt;th&gt;cursor stability&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Postgres&lt;/td&gt;
&lt;td&gt;~5 ms&lt;/td&gt;
&lt;td&gt;~5 ms at any depth&lt;/td&gt;
&lt;td&gt;perfect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snowflake&lt;/td&gt;
&lt;td&gt;~200 ms&lt;/td&gt;
&lt;td&gt;~200 ms at any depth&lt;/td&gt;
&lt;td&gt;perfect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BigQuery&lt;/td&gt;
&lt;td&gt;~150 ms&lt;/td&gt;
&lt;td&gt;~150 ms at any depth&lt;/td&gt;
&lt;td&gt;perfect&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;One cursor contract, three plan shapes&lt;/strong&gt;&lt;/strong&gt; — the API contract is stable (&lt;code&gt;{cursor_ts, cursor_id, page_size}&lt;/code&gt;), the underlying plan adapts per engine. Callers never learn which warehouse is behind the endpoint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Compile-time dispatch, zero runtime overhead&lt;/strong&gt;&lt;/strong&gt; — dbt evaluates the Jinja block at compile time. The emitted SQL is one of three variants; no runtime branching, no reflection, no penalty.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Postgres gets the covering btree&lt;/strong&gt;&lt;/strong&gt; — the composite index &lt;code&gt;(created_at DESC, id DESC)&lt;/code&gt; is index-aligned with the ORDER BY, so the plan is index scan + LIMIT with no sort step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Snowflake gets the clustering key&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;CLUSTER BY (created_at)&lt;/code&gt; on the orders table plus the tuple predicate prunes to a small set of micro-partitions per page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;BigQuery gets partition + cluster pruning&lt;/strong&gt;&lt;/strong&gt; — partition the table by &lt;code&gt;DATE(created_at)&lt;/code&gt; and cluster by &lt;code&gt;created_at&lt;/code&gt;. The 90-day window on the outer WHERE prunes at partition level; the tuple predicate then prunes within clusters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — Postgres is &lt;code&gt;O(log n + K)&lt;/code&gt; per page, Snowflake and BigQuery are &lt;code&gt;O(P + K)&lt;/code&gt; where P is the number of relevant partitions/clusters (typically 1–3 per page). All three are effectively flat across page depth — the whole point of keyset.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — SQL&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL problem library — 450+ DE-focused questions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — order by / limit&lt;/span&gt;
&lt;strong&gt;ORDER BY + LIMIT drills&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/order-by-limit" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  3. Why OFFSET is slow at page 1M
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;sql limit offset performance&lt;/code&gt; — the O(k + n) cost model, the wide-row memory story, and the "consistent read across pages" failure mode
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;&lt;code&gt;sql limit offset performance&lt;/code&gt; is &lt;code&gt;O(k + n)&lt;/code&gt; per page because the planner must walk the ordered scan past &lt;code&gt;k&lt;/code&gt; rows and discard them before emitting the next &lt;code&gt;n&lt;/code&gt;, so a page at depth &lt;code&gt;k=1,000,000&lt;/code&gt; costs 50,000× more than a page at depth &lt;code&gt;k=20&lt;/code&gt; — a curve that stays flat until it isn't and then climbs a cliff&lt;/strong&gt;. Everything you say about OFFSET at scale is a corollary of this cost model.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbzrtkxxu78rkvcm9dhfq.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbzrtkxxu78rkvcm9dhfq.jpeg" alt="Visual diagram of why OFFSET is slow at page 1M — left a plan sketch showing the planner scanning 1M rows and discarding them before returning 20; centre a benchmark curve chart with a flat region up to page 100 then a cliff at page 100K; right a rules-of-thumb card with the O(k + n) cost formula and a memory-pressure annotation; on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Slot 1 — the planner has to walk the scan.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OFFSET is not a "skip pointer." No engine in 2026 (row-store or columnar) has an O(1) skip-to-position primitive on an ordered scan.&lt;/li&gt;
&lt;li&gt;The plan for &lt;code&gt;SELECT ... ORDER BY x LIMIT n OFFSET k&lt;/code&gt; on Postgres is &lt;code&gt;Limit (n rows) → Index Scan Backward using idx_x&lt;/code&gt;. The Limit step reads &lt;code&gt;k + n&lt;/code&gt; rows from the index scan; the first &lt;code&gt;k&lt;/code&gt; are counted-then-discarded; the last &lt;code&gt;n&lt;/code&gt; are emitted.&lt;/li&gt;
&lt;li&gt;Cost is &lt;code&gt;O(k + n)&lt;/code&gt; per page. For a fixed &lt;code&gt;n = 20&lt;/code&gt;, cost is effectively &lt;code&gt;O(k)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Total cost across pages 1 through P is &lt;code&gt;Σ (k_i + n) = Σ ((i-1) × n + n) = n × P × (P+1) / 2&lt;/code&gt; = &lt;code&gt;O(P² × n)&lt;/code&gt;. Quadratic in the number of pages fetched.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 2 — wide rows amplify the pain.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every scanned-and-discarded row still incurs I/O for the columns projected in the SELECT list. &lt;code&gt;SELECT * FROM t OFFSET 1000000&lt;/code&gt; on a 1KB-per-row table reads ~1GB before emitting the first byte.&lt;/li&gt;
&lt;li&gt;Even with &lt;code&gt;SELECT id, created_at&lt;/code&gt; (a covering-index-only query), the index leaf pages must be read — smaller than the heap but non-zero.&lt;/li&gt;
&lt;li&gt;Wide rows also amplify sort memory. If the ORDER BY isn't index-aligned, the query needs an explicit sort — a sort of &lt;code&gt;k + n&lt;/code&gt; rows fitting in &lt;code&gt;sort_mem&lt;/code&gt; (Postgres &lt;code&gt;work_mem&lt;/code&gt;) or spilling to disk. Deep OFFSET on a non-indexed ORDER BY is catastrophic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 3 — the benchmark curve.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On a 10M-row Postgres table with covering btree &lt;code&gt;(created_at DESC, id DESC)&lt;/code&gt;:

&lt;ul&gt;
&lt;li&gt;Page 1 (&lt;code&gt;OFFSET 0 LIMIT 20&lt;/code&gt;) — ~3 ms.&lt;/li&gt;
&lt;li&gt;Page 100 (&lt;code&gt;OFFSET 1,980&lt;/code&gt;) — ~4 ms.&lt;/li&gt;
&lt;li&gt;Page 1,000 (&lt;code&gt;OFFSET 19,980&lt;/code&gt;) — ~15 ms.&lt;/li&gt;
&lt;li&gt;Page 10,000 (&lt;code&gt;OFFSET 199,980&lt;/code&gt;) — ~140 ms.&lt;/li&gt;
&lt;li&gt;Page 100,000 (&lt;code&gt;OFFSET 1,999,980&lt;/code&gt;) — ~1.4 s.&lt;/li&gt;
&lt;li&gt;Page 500,000 (&lt;code&gt;OFFSET 9,999,980&lt;/code&gt;) — ~7 s (hits shared-buffer pressure).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;The curve is roughly linear in &lt;code&gt;k&lt;/code&gt; until buffer pressure kicks in, then super-linear.&lt;/li&gt;
&lt;li&gt;On columnar warehouses (BigQuery, Snowflake), the same curve is shifted upward by 5–10× because of per-column read overhead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 4 — the "consistent read across pages" problem.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OFFSET is &lt;em&gt;positional&lt;/em&gt; — it counts rows by their ordinal in the ORDER BY. If a row is inserted before the current position, the ordinal of every subsequent row shifts by one.&lt;/li&gt;
&lt;li&gt;Duplicate read — a new row lands above the last cursor position between pages. The old row 21 now becomes new row 22; &lt;code&gt;OFFSET 20 LIMIT 20&lt;/code&gt; returns the old row 20 as its first row on page 2. The user sees it twice.&lt;/li&gt;
&lt;li&gt;Skipped read — a row is deleted between pages. The old row 21 disappears; new row 21 is what was old row 22. &lt;code&gt;OFFSET 20 LIMIT 20&lt;/code&gt; returns starting from new row 21 (= old row 22), skipping what the user should have seen.&lt;/li&gt;
&lt;li&gt;Both failure modes are silent — no error is thrown, no log line is emitted. The client just sees inconsistent data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 5 — mitigations that don't fix the fundamental problem.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Serialisable transactions.&lt;/strong&gt; Wrapping the paginator in &lt;code&gt;BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE&lt;/code&gt; gives a stable snapshot for all reads. Works, but holds locks and pins MVCC snapshots across HTTP calls — hostile to any web-scale system.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snapshot cursors.&lt;/strong&gt; Server-side declared cursors (Postgres &lt;code&gt;DECLARE CURSOR ... FOR ...&lt;/code&gt;, SQL Server cursor handles). Same problem — stateful on the DB side, needs sticky sessions, doesn't survive connection pooling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Row versioning / audit tables.&lt;/strong&gt; Add &lt;code&gt;deleted_at&lt;/code&gt; and &lt;code&gt;updated_at&lt;/code&gt; columns; filter out rows deleted after the initial page. Half a fix — new inserts still shift positions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cap the max page.&lt;/strong&gt; Facebook / LinkedIn "5,000 pages max" hard cap. Doesn't fix drift, just bounds the pain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keyset pagination.&lt;/strong&gt; The real fix. Navigate by key, not by position. Immune to inserts, deletes, and updates by design.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 6 — when OFFSET is actually fine.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Small tables (&amp;lt; 1M rows). The whole table fits in RAM; even deep offsets are cheap.&lt;/li&gt;
&lt;li&gt;Aggregate tables in warehouses. Superset / Looker dashboards on 10K-row summary tables. Fine.&lt;/li&gt;
&lt;li&gt;Admin dashboards with page caps. If your admin UI caps the pager at page 500, deep OFFSET stays bounded.&lt;/li&gt;
&lt;li&gt;Batched exports. The exporter reads pages 1 → P sequentially; total cost is quadratic, but if &lt;code&gt;P&lt;/code&gt; is small the constant hides it.&lt;/li&gt;
&lt;li&gt;Prototyping. Ship OFFSET, get the UI working, replace with keyset before scaling. This is the standard trajectory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 7 — reading the EXPLAIN plan.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Postgres &lt;code&gt;EXPLAIN (ANALYZE, BUFFERS)&lt;/code&gt; — look for &lt;code&gt;Buffers: shared read=N&lt;/code&gt; on the Limit step. &lt;code&gt;N&lt;/code&gt; grows linearly with &lt;code&gt;k&lt;/code&gt; for OFFSET; stays flat for keyset.&lt;/li&gt;
&lt;li&gt;MySQL 8 &lt;code&gt;EXPLAIN FORMAT=TREE&lt;/code&gt; — look for &lt;code&gt;rows examined&lt;/code&gt; on the scan. Same story.&lt;/li&gt;
&lt;li&gt;SQL Server &lt;code&gt;SET STATISTICS IO ON&lt;/code&gt; — the &lt;code&gt;logical reads&lt;/code&gt; count grows with &lt;code&gt;k&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Snowflake — the query profile shows &lt;code&gt;Rows Scanned&lt;/code&gt; on the source table. For &lt;code&gt;OFFSET 1M LIMIT 20&lt;/code&gt;, &lt;code&gt;Rows Scanned&lt;/code&gt; is ~1M; for keyset, it's ~20 (post-prune).&lt;/li&gt;
&lt;li&gt;BigQuery — the "Bytes billed" tab shows OFFSET reads the full pre-limit column data. Keyset reads dramatically less.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common newbie mistakes.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Assuming OFFSET is O(1). It's O(k + n). Every deep-page complaint traces back to this misconception.&lt;/li&gt;
&lt;li&gt;Not adding a tie-breaker. Rows with duplicate ORDER BY keys drift across pages.&lt;/li&gt;
&lt;li&gt;Not adding &lt;code&gt;NULLS LAST&lt;/code&gt;. NULLs appear or disappear across pages depending on engine defaults.&lt;/li&gt;
&lt;li&gt;Trusting OFFSET under concurrent writes. It drifts — silently.&lt;/li&gt;
&lt;li&gt;Assuming the covering index makes OFFSET fast. It makes it &lt;em&gt;faster&lt;/em&gt; than a sort-and-discard, but still O(k). The keyset path is O(log n + K) — dramatically different.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — measuring the OFFSET latency curve with EXPLAIN ANALYZE
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The clearest demonstration — run &lt;code&gt;EXPLAIN (ANALYZE, BUFFERS)&lt;/code&gt; at pages 1, 100, 1K, 10K, 100K, and 1M on the same query, then read the numbers. Every senior engineer has done this at least once; the interview question is often "walk me through what changes."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given a Postgres &lt;code&gt;events(id, user_id, created_at, payload)&lt;/code&gt; table with 10M rows and a covering btree on &lt;code&gt;(created_at DESC, id DESC)&lt;/code&gt;, run &lt;code&gt;EXPLAIN (ANALYZE, BUFFERS)&lt;/code&gt; for &lt;code&gt;LIMIT 20 OFFSET k&lt;/code&gt; at k = 0, 20,000, 200,000, and 2,000,000. Report the latency curve.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt; (10M-row events table, indexed on &lt;code&gt;(created_at DESC, id DESC)&lt;/code&gt;.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUFFERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Limit  (cost=0.43..1.28 rows=20)&lt;/span&gt;
&lt;span class="c1"&gt;--   Buffers: shared hit=6&lt;/span&gt;
&lt;span class="c1"&gt;--   -&amp;gt; Index Scan Backward using idx_events (cost=0.43..425000.00 rows=10000000)&lt;/span&gt;
&lt;span class="c1"&gt;-- Planning Time: 0.1 ms&lt;/span&gt;
&lt;span class="c1"&gt;-- Execution Time: 3 ms&lt;/span&gt;

&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUFFERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="mi"&gt;20000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Limit  (cost=849.30..850.15 rows=20)&lt;/span&gt;
&lt;span class="c1"&gt;--   Buffers: shared hit=1200 read=800&lt;/span&gt;
&lt;span class="c1"&gt;--   Execution Time: 45 ms&lt;/span&gt;

&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUFFERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="mi"&gt;200000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Limit  (cost=8493.00..8493.85 rows=20)&lt;/span&gt;
&lt;span class="c1"&gt;--   Buffers: shared hit=8000 read=12000&lt;/span&gt;
&lt;span class="c1"&gt;--   Execution Time: 380 ms&lt;/span&gt;

&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUFFERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="mi"&gt;2000000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Limit  (cost=84930.00..84930.85 rows=20)&lt;/span&gt;
&lt;span class="c1"&gt;--   Buffers: shared hit=50000 read=150000&lt;/span&gt;
&lt;span class="c1"&gt;--   Execution Time: 3800 ms&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Page 1 (&lt;code&gt;OFFSET 0&lt;/code&gt;) — 6 buffer hits, 3 ms. The plan reads 20 index leaves and emits 20 rows. Best case.&lt;/li&gt;
&lt;li&gt;Page 1,001 (&lt;code&gt;OFFSET 20,000&lt;/code&gt;) — 2,000 buffer accesses, 45 ms. Postgres walks 20,000 index leaves before emitting. Latency grows roughly linearly with k.&lt;/li&gt;
&lt;li&gt;Page 10,001 (&lt;code&gt;OFFSET 200,000&lt;/code&gt;) — 20,000 buffer accesses, 380 ms. &lt;code&gt;shared read&lt;/code&gt; (buffer misses) start climbing because the working set exceeds &lt;code&gt;shared_buffers&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Page 100,001 (&lt;code&gt;OFFSET 2,000,000&lt;/code&gt;) — 200,000 buffer accesses, 3.8 s. Almost all reads are &lt;code&gt;shared read&lt;/code&gt; — buffer pool saturated. This is the "someone will page the on-call" latency.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;Buffers: shared read&lt;/code&gt; count is the smoking gun — for OFFSET, it grows linearly with &lt;code&gt;k&lt;/code&gt;; for keyset, it stays flat at 6–10 per page regardless of depth.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output&lt;/strong&gt; — latency curve.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Page (OFFSET)&lt;/th&gt;
&lt;th&gt;Buffers&lt;/th&gt;
&lt;th&gt;Latency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 (0)&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;3 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,001 (20,000)&lt;/td&gt;
&lt;td&gt;2,000&lt;/td&gt;
&lt;td&gt;45 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10,001 (200,000)&lt;/td&gt;
&lt;td&gt;20,000&lt;/td&gt;
&lt;td&gt;380 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100,001 (2,000,000)&lt;/td&gt;
&lt;td&gt;200,000&lt;/td&gt;
&lt;td&gt;3,800 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Deep OFFSET's latency curve is linear-then-super-linear. Everything under &lt;code&gt;k ≤ 10,000&lt;/code&gt; on a covering-indexed table is fine; everything past &lt;code&gt;k ≥ 100,000&lt;/code&gt; is a support ticket waiting to happen. Add a hard cap or migrate to keyset — the choice is not "if" but "when."&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the concurrent-write drift bug in action
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The most infuriating pagination bug: pages that "shift" as users read them. This example builds a minimal reproduction using two sessions and shows exactly which row is missed and which row is duplicated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Simulate two sessions — session A reads page 1 of an OFFSET paginator, session B inserts a new row, session A reads page 2. Show which row is duplicated. Then show the same scenario with a delete instead of an insert.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt; Rows 1–40 in &lt;code&gt;events&lt;/code&gt;, sorted DESC by &lt;code&gt;created_at&lt;/code&gt;, all with a unique &lt;code&gt;created_at&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Session A: page 1&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Returns: 40, 39, 38, ..., 21&lt;/span&gt;

&lt;span class="c1"&gt;-- Session B: insert a new row with created_at newer than any existing&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;41&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2026-07-11 00:00'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Session A: page 2 (OFFSET drifted)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Returns: 21, 20, 19, ..., 2&lt;/span&gt;
&lt;span class="c1"&gt;-- id=21 IS SEEN TWICE (once at end of page 1, once at start of page 2)&lt;/span&gt;


&lt;span class="c1"&gt;-- Reset. Repeat with delete.&lt;/span&gt;
&lt;span class="c1"&gt;-- Session A: page 1&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Returns: 40, 39, ..., 21&lt;/span&gt;

&lt;span class="c1"&gt;-- Session B: delete row 30 (which was on page 1)&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Session A: page 2&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Returns: 20, 19, ..., 1&lt;/span&gt;
&lt;span class="c1"&gt;-- id=21 is SKIPPED (it should have been on page 2 but the offset now starts at 20)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pre-insert state — 40 rows sorted DESC. Page 1 (offset 0, limit 20) returns rows 40 → 21.&lt;/li&gt;
&lt;li&gt;Session B inserts id=41 with a newer timestamp. The ordered stream is now &lt;code&gt;41, 40, 39, ..., 1&lt;/code&gt; — 41 rows total.&lt;/li&gt;
&lt;li&gt;Session A calls page 2 (offset 20, limit 20). Offset 20 in the new stream is row 21 (&lt;code&gt;41, 40, ..., 22&lt;/code&gt; is the first 20). Page 2 returns &lt;code&gt;21, 20, ..., 2&lt;/code&gt; — the user sees id=21 twice.&lt;/li&gt;
&lt;li&gt;In the delete scenario, session B deletes id=30 from the middle of page 1. The stream becomes &lt;code&gt;40, 39, ..., 31, 29, 28, ..., 1&lt;/code&gt; — 39 rows.&lt;/li&gt;
&lt;li&gt;Session A calls page 2. Offset 20 in the shrunken stream is row 21 in the original stream, which is now... row 20 in the new stream (since one was deleted). The paginator emits &lt;code&gt;20, 19, ..., 1&lt;/code&gt; — id=21 is &lt;em&gt;skipped&lt;/em&gt; entirely.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt; Both scenarios produce inconsistent data — one duplicates, one skips.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Any OFFSET paginator against a mutable source has this drift. Every social feed, every "list of my orders" endpoint, every "list of my repos" page uses keyset instead. Keyset navigates by &lt;em&gt;key&lt;/em&gt; (the last row's tuple), so inserts and deletes elsewhere in the ordered stream have no effect on subsequent pages.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — bounded-OFFSET as a safety net
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; If you can't migrate to keyset today (legacy admin dashboard, page-numbered pager), the cheap fix is a hard cap. Facebook, LinkedIn, and Google search results all do this — they refuse to serve past page N.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Design a bounded-OFFSET endpoint that caps at 5,000 pages of 100 rows each. What are the trade-offs?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt; A 100M-row orders table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;list_orders_capped&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page_size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Hard caps
&lt;/span&gt;    &lt;span class="n"&gt;MAX_PAGE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5_000&lt;/span&gt;
    &lt;span class="n"&gt;MAX_PAGE_SIZE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MAX_PAGE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HttpError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Page number exceeds maximum. Use search or filters.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;page_size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MAX_PAGE_SIZE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;page_size&lt;/span&gt;

    &lt;span class="c1"&gt;# Worst case: page=5000, page_size=200 =&amp;gt; OFFSET 1,000,000 LIMIT 200
&lt;/span&gt;    &lt;span class="c1"&gt;# Bounded ~600ms on a covering-indexed table
&lt;/span&gt;    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        SELECT id, created_at, total
        FROM orders
        ORDER BY created_at DESC, id DESC
        LIMIT %s OFFSET %s
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;MAX_PAGE = 5000&lt;/code&gt; cap means the largest OFFSET is &lt;code&gt;5000 × 200 = 1,000,000&lt;/code&gt;. Bounded worst case.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;MAX_PAGE_SIZE = 200&lt;/code&gt; cap means no client can ask for a 10K-row page and force a &lt;code&gt;LIMIT 10000 OFFSET 100000&lt;/code&gt; monster query.&lt;/li&gt;
&lt;li&gt;If the client asks for page 5,001, the endpoint returns a 400 with a hint to use search filters (or infinite scroll via a keyset endpoint).&lt;/li&gt;
&lt;li&gt;Deep-page latency at the cap is bounded — ~600 ms on a covering-indexed 100M-row table. Acceptable for an admin UI; unacceptable for consumer feeds (that's why consumer uses keyset).&lt;/li&gt;
&lt;li&gt;This is the &lt;em&gt;Facebook-search&lt;/em&gt; pattern — "your search returned too many results, please refine your query" is not a UX failure, it is a deliberate cost boundary.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Client on page ≤ 5,000 — normal response, bounded latency.&lt;/li&gt;
&lt;li&gt;Client on page &amp;gt; 5,000 — 400 error with guidance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Bounded OFFSET is the "we can't migrate today" fix. It doesn't cure the drift problem (concurrent writes still cause duplicates / skips), but it caps the perf blast radius. Every legacy admin dashboard in production ships this pattern.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on OFFSET perf diagnosis
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "You get a Slack ping — the &lt;code&gt;orders&lt;/code&gt; list page is 8 seconds at page 500. The engineer who wrote it says 'we have an index.' Diagnose the failure in under two minutes."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using EXPLAIN ANALYZE + covering-index verification + keyset migration path
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Step 1: EXPLAIN the failing query&lt;/span&gt;
&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUFFERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Look for:&lt;/span&gt;
&lt;span class="c1"&gt;--   1. Is the scan using the index? (Index Scan Backward using idx_orders_created_at)&lt;/span&gt;
&lt;span class="c1"&gt;--   2. Is the sort happening in the executor? (Sort node → covering index missing / mis-aligned)&lt;/span&gt;
&lt;span class="c1"&gt;--   3. Buffers: shared read count (growing with OFFSET = OFFSET trap)&lt;/span&gt;
&lt;span class="c1"&gt;--   4. Any Filter step after the index scan (missing covering columns)&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 2: Verify the index actually covers the ORDER BY&lt;/span&gt;
&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="c1"&gt;-- If the existing index is (created_at) but the ORDER BY is (created_at DESC, id DESC),&lt;/span&gt;
&lt;span class="c1"&gt;-- Postgres 15+ can still walk backward but old versions do a sort.&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 3: Add the covering composite if missing&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;CONCURRENTLY&lt;/span&gt; &lt;span class="n"&gt;idx_orders_paginate&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- Now the Limit → Index Scan Backward path is optimal for OFFSET too.&lt;/span&gt;
&lt;span class="c1"&gt;-- But OFFSET remains O(k) — indexing does not fix the trap.&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 4: Migrate to keyset for the consumer endpoint&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(:&lt;/span&gt;&lt;span class="n"&gt;cursor_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;cursor_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Now O(log n + K) at any depth.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;What you check&lt;/th&gt;
&lt;th&gt;What it tells you&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;EXPLAIN plan node type&lt;/td&gt;
&lt;td&gt;Sort node = missing / mis-aligned index; Index Scan Backward = good&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Buffers: shared read&lt;/td&gt;
&lt;td&gt;Growing with OFFSET = classic OFFSET trap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;\d+ orders index list&lt;/td&gt;
&lt;td&gt;Composite &lt;code&gt;(created_at DESC, id DESC)&lt;/code&gt; = optimal; single-col = suboptimal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Actual rows vs actual loops&lt;/td&gt;
&lt;td&gt;20 rows but 10,020 loops = classic OFFSET scan-and-discard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Fix path A (short-term)&lt;/td&gt;
&lt;td&gt;CREATE INDEX CONCURRENTLY (covering)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Fix path B (long-term)&lt;/td&gt;
&lt;td&gt;Migrate to keyset for consumer endpoints&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The senior diagnosis is: identify the OFFSET trap in EXPLAIN, verify the covering index is in place (adding it if missing), and set the migration to keyset for any endpoint expected to serve deep pages. The short-term win is a 2–3× improvement from index alignment; the long-term win is O(log n) from keyset — a 100× improvement at page 100K.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;th&gt;Page 500 latency&lt;/th&gt;
&lt;th&gt;Page 100K latency&lt;/th&gt;
&lt;th&gt;Sustainable?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Baseline (missing composite index)&lt;/td&gt;
&lt;td&gt;8 s&lt;/td&gt;
&lt;td&gt;Timeout&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Composite index added&lt;/td&gt;
&lt;td&gt;300 ms&lt;/td&gt;
&lt;td&gt;4 s&lt;/td&gt;
&lt;td&gt;For admin UI with page cap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Migrated to keyset&lt;/td&gt;
&lt;td&gt;8 ms&lt;/td&gt;
&lt;td&gt;8 ms&lt;/td&gt;
&lt;td&gt;Yes — flat forever&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;EXPLAIN ANALYZE reveals the plan shape&lt;/strong&gt;&lt;/strong&gt; — the &lt;code&gt;actual rows vs actual loops&lt;/code&gt; line is diagnostic. A LIMIT of 20 that runs 10,000 loops is the classic OFFSET signature. Every senior engineer reads this line first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Composite index alignment&lt;/strong&gt;&lt;/strong&gt; — the ORDER BY &lt;code&gt;(created_at DESC, id DESC)&lt;/code&gt; matches a composite index &lt;code&gt;(created_at DESC, id DESC)&lt;/code&gt;; Postgres walks the index leaves backward without a Sort node. Reducing the sort step is the cheapest short-term win.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;OFFSET is O(k) even with covering index&lt;/strong&gt;&lt;/strong&gt; — indexing does not fix the trap; it just makes the scan-and-discard cheaper per row. At &lt;code&gt;k = 100,000&lt;/code&gt; on a covering-indexed table, latency is still seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Keyset is O(log n + K)&lt;/strong&gt;&lt;/strong&gt; — the tuple predicate uses the same composite index as a seek. The plan is &lt;code&gt;Index Scan Backward&lt;/code&gt; with &lt;code&gt;actual loops = 20&lt;/code&gt;, regardless of depth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — Baseline (no composite) is &lt;code&gt;O(k × log n)&lt;/code&gt; with sort; composite-only is &lt;code&gt;O(k)&lt;/code&gt;; keyset is &lt;code&gt;O(log n + K)&lt;/code&gt;. On a 100M-row table with K=20 and k=100,000, the numbers are ~40 s / ~4 s / ~10 ms — a 400× spread across the three fixes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — indexing&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL indexing drills&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/indexing" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — optimization&lt;/span&gt;
&lt;strong&gt;SQL optimization problems&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/optimization" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  4. Keyset (seek method) pagination
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;keyset pagination&lt;/code&gt; and &lt;code&gt;seek method pagination&lt;/code&gt; — WHERE (created_at, id) &amp;lt; (:cursor) ORDER BY DESC LIMIT n, composite index alignment, and the O(log n) deep-page path
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;&lt;code&gt;keyset pagination&lt;/code&gt; (a.k.a. &lt;code&gt;seek method pagination&lt;/code&gt;) replaces "skip k rows" with "give me the rows strictly &lt;em&gt;after&lt;/em&gt; this cursor tuple," where the cursor is the last row of the previous page and the comparison is a lexicographic tuple compare — turning the deep-page cost from &lt;code&gt;O(k + n)&lt;/code&gt; (scan + discard) into &lt;code&gt;O(log n + K)&lt;/code&gt; (one index seek + K sequential reads)&lt;/strong&gt;. Every modern feed, every GraphQL Relay endpoint, and every "next 20" API on the internet is built on this primitive.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feepnvsfbvlimbobdxhhh.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feepnvsfbvlimbobdxhhh.jpeg" alt="Visual diagram of keyset (seek method) pagination — left an index card showing a composite btree on (created_at, id) with a seek arrow to a cursor position; centre a WHERE-tuple-comparison card WHERE (created_at, id) &lt; (:cursor_ts, :cursor_id) ORDER BY created_at DESC, id DESC LIMIT 20; right a stability card showing rows remaining stable under inserts and deletes; on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Slot 1 — the keyset skeleton.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two-slot template: &lt;strong&gt;Slot A&lt;/strong&gt; — the WHERE tuple comparison: &lt;code&gt;WHERE (order_col, id) &amp;lt; (:cursor_col, :cursor_id)&lt;/code&gt; for DESC pagination, &lt;code&gt;&amp;gt;&lt;/code&gt; for ASC. &lt;strong&gt;Slot B&lt;/strong&gt; — the ORDER BY with matching direction and tie-breaker: &lt;code&gt;ORDER BY order_col DESC, id DESC LIMIT n&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Every keyset query fills the two slots. The cursor is &lt;code&gt;(last_row.order_col, last_row.id)&lt;/code&gt; from the previous page — no &lt;code&gt;OFFSET&lt;/code&gt;, no page number.&lt;/li&gt;
&lt;li&gt;The tuple comparison &lt;code&gt;(a, b) &amp;lt; (:a, :b)&lt;/code&gt; is &lt;em&gt;lexicographic&lt;/em&gt; — first &lt;code&gt;a &amp;lt; :a&lt;/code&gt;, or &lt;code&gt;a = :a AND b &amp;lt; :b&lt;/code&gt;. This is &lt;em&gt;not&lt;/em&gt; the same as &lt;code&gt;a &amp;lt; :a AND b &amp;lt; :b&lt;/code&gt; — that comparison would drop rows where &lt;code&gt;a = :a AND b &amp;lt; :b&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 2 — tuple comparison is one-shot, not per-column.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Postgres, MySQL 8, Oracle 12c+, Snowflake, and BigQuery all support &lt;code&gt;WHERE (a, b) &amp;lt; (:a, :b)&lt;/code&gt; as native row-value comparison. This is the ANSI SQL:1999 spelling.&lt;/li&gt;
&lt;li&gt;SQL Server and older MySQL don't support row-value comparison. The equivalent is &lt;code&gt;WHERE a &amp;lt; :a OR (a = :a AND b &amp;lt; :b)&lt;/code&gt; — verbose but semantically identical.&lt;/li&gt;
&lt;li&gt;The one-shot form is not just a syntactic nicety — it lets the planner recognise the comparison as a &lt;em&gt;range scan&lt;/em&gt; on the composite index &lt;code&gt;(a, b)&lt;/code&gt; and use a single index seek. The expanded per-column form may or may not compile to the same plan depending on planner cleverness.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 3 — composite index alignment.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The composite index &lt;code&gt;(order_col, id)&lt;/code&gt; in the &lt;em&gt;same direction&lt;/em&gt; as the ORDER BY (&lt;code&gt;DESC&lt;/code&gt; or &lt;code&gt;ASC&lt;/code&gt;) enables an index-only backward or forward scan.&lt;/li&gt;
&lt;li&gt;Postgres 15+ can walk any btree in either direction, so &lt;code&gt;CREATE INDEX ... ON t (created_at DESC, id DESC)&lt;/code&gt; and &lt;code&gt;CREATE INDEX ... ON t (created_at ASC, id ASC)&lt;/code&gt; work equivalently for backward scans.&lt;/li&gt;
&lt;li&gt;MySQL 8.0.13+ supports descending indexes (&lt;code&gt;CREATE INDEX ... ON t (created_at DESC, id DESC)&lt;/code&gt;). Older MySQL always stored indexes ascending — the planner walked them backward for DESC scans, which was slightly less efficient.&lt;/li&gt;
&lt;li&gt;SQL Server has supported descending index columns since forever. Oracle since 8i.&lt;/li&gt;
&lt;li&gt;Snowflake has no btree — the equivalent is &lt;code&gt;CLUSTER BY (created_at)&lt;/code&gt;, which reorders micro-partitions to co-locate rows with similar &lt;code&gt;created_at&lt;/code&gt; values.&lt;/li&gt;
&lt;li&gt;BigQuery uses table clustering — &lt;code&gt;CLUSTER BY created_at&lt;/code&gt; — which does the same physical co-location.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 4 — the O(log n + K) deep-page path.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The index seek on &lt;code&gt;(created_at, id) &amp;lt; (:ts, :id)&lt;/code&gt; positions the scan at exactly the right leaf page in &lt;code&gt;O(log n)&lt;/code&gt; — the depth of the btree.&lt;/li&gt;
&lt;li&gt;From that leaf, the engine reads &lt;em&gt;K&lt;/em&gt; sequential rows (where K is the LIMIT). Each read is O(1). Total forward-scan cost is &lt;code&gt;O(K)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Overall: &lt;code&gt;O(log n + K)&lt;/code&gt; per page — flat in the depth of the pagination. Page 1 and page 1,000,000 both cost the same.&lt;/li&gt;
&lt;li&gt;Compared to OFFSET's &lt;code&gt;O(k + n)&lt;/code&gt; — at page 100K with &lt;code&gt;n = 20&lt;/code&gt;, keyset is 5,000× faster.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 5 — DESC pagination vs ASC pagination.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DESC&lt;/strong&gt; (newest-first feed) — &lt;code&gt;WHERE (created_at, id) &amp;lt; (:cursor_ts, :cursor_id) ORDER BY created_at DESC, id DESC LIMIT n&lt;/code&gt;. Cursor is the last (oldest) row emitted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ASC&lt;/strong&gt; (oldest-first log) — &lt;code&gt;WHERE (created_at, id) &amp;gt; (:cursor_ts, :cursor_id) ORDER BY created_at ASC, id ASC LIMIT n&lt;/code&gt;. Cursor is the last (newest) row emitted.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;&amp;lt;&lt;/code&gt; / &lt;code&gt;&amp;gt;&lt;/code&gt; operator flips with the direction; the ORDER BY direction flips too; the tie-breaker direction must match.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mixing directions&lt;/strong&gt; — &lt;code&gt;ORDER BY created_at DESC, id ASC&lt;/code&gt; — is possible but requires the tuple comparison and the ORDER BY to match. The composite index must be &lt;code&gt;(created_at DESC, id ASC)&lt;/code&gt; or Postgres can't do an index-only scan.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 6 — bidirectional pagination.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Forward — &lt;code&gt;WHERE (a, b) &amp;lt; (:a, :b) ORDER BY a DESC, b DESC LIMIT n&lt;/code&gt;. Returns the next N older rows.&lt;/li&gt;
&lt;li&gt;Backward — &lt;code&gt;WHERE (a, b) &amp;gt; (:a, :b) ORDER BY a ASC, b ASC LIMIT n&lt;/code&gt;, then reverse the result set in application code. Returns the previous N newer rows.&lt;/li&gt;
&lt;li&gt;The Relay spec's &lt;code&gt;first / after / last / before&lt;/code&gt; maps directly to this — &lt;code&gt;first + after&lt;/code&gt; is forward, &lt;code&gt;last + before&lt;/code&gt; is backward.&lt;/li&gt;
&lt;li&gt;Some engines support the &lt;code&gt;ORDER BY a ASC, b ASC LIMIT n&lt;/code&gt; variant with the &lt;em&gt;result&lt;/em&gt; reversed — Postgres does this via a subquery: &lt;code&gt;SELECT * FROM (... ORDER BY a ASC LIMIT n) t ORDER BY a DESC&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 7 — trade-offs and honest limitations.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No random access.&lt;/strong&gt; You can't ask for "page 47" — keyset only lets you paginate forward or backward from a known cursor. The trade-off is: no page numbers, no total-count-of-pages banner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Total row count is expensive.&lt;/strong&gt; Keyset gives you &lt;code&gt;hasNext&lt;/code&gt; cheaply (via LIMIT n+1 peek-ahead) but no total. Total count on a mutable table is &lt;code&gt;O(n)&lt;/code&gt; anyway — the OFFSET pager pays that cost only because the total-count query is a separate &lt;code&gt;SELECT COUNT(*)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skipping a page is impossible.&lt;/strong&gt; Client-side "jump to page 100" doesn't map to keyset. The only workaround is a hybrid — keyset for infinite scroll, bounded OFFSET for admin dashboards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cursor must include all ORDER BY columns.&lt;/strong&gt; If the ORDER BY is &lt;code&gt;(created_at, id)&lt;/code&gt;, the cursor is &lt;code&gt;(created_at, id)&lt;/code&gt;. Adding a third sort column adds a third cursor component.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 8 — handling NULLs correctly.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If &lt;code&gt;created_at&lt;/code&gt; can be NULL, the tuple comparison behaves unexpectedly. On most engines, &lt;code&gt;NULL &amp;lt; anything&lt;/code&gt; and &lt;code&gt;NULL &amp;gt; anything&lt;/code&gt; both return NULL — the row is &lt;em&gt;not&lt;/em&gt; returned.&lt;/li&gt;
&lt;li&gt;Fix — use &lt;code&gt;ORDER BY created_at DESC NULLS LAST&lt;/code&gt; and, in the WHERE, use a &lt;code&gt;COALESCE(created_at, '-infinity')&lt;/code&gt; cast to give NULLs a defined position in the order.&lt;/li&gt;
&lt;li&gt;Better fix — never allow NULLs on the paginated sort column. Make it &lt;code&gt;NOT NULL&lt;/code&gt; and populate on insert.&lt;/li&gt;
&lt;li&gt;On BigQuery, the default for &lt;code&gt;ORDER BY x DESC&lt;/code&gt; is NULLs &lt;em&gt;first&lt;/em&gt;. Use &lt;code&gt;NULLS LAST&lt;/code&gt; explicitly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 9 — handling duplicate ORDER BY keys (ties).&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two rows with the same &lt;code&gt;created_at&lt;/code&gt; — the tie-breaker on &lt;code&gt;id&lt;/code&gt; disambiguates. &lt;code&gt;WHERE (created_at, id) &amp;lt; (:ts, :id)&lt;/code&gt; correctly excludes only the exact cursor row and continues from there.&lt;/li&gt;
&lt;li&gt;Without the &lt;code&gt;id&lt;/code&gt; tie-breaker, &lt;code&gt;WHERE created_at &amp;lt; :ts&lt;/code&gt; would skip &lt;em&gt;all&lt;/em&gt; rows with &lt;code&gt;created_at = :ts&lt;/code&gt; — you might miss valid rows.&lt;/li&gt;
&lt;li&gt;The tie-breaker column must be &lt;em&gt;unique&lt;/em&gt; across the table (usually the primary key &lt;code&gt;id&lt;/code&gt;) — otherwise the composite comparison can still tie and cause drift.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 10 — cursor stability under mutations.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;New insert with &lt;code&gt;created_at &amp;gt; cursor&lt;/code&gt; — the cursor's tuple stays the same; the new row appears on future backward pages (relative to a newer cursor), never on already-paginated results. Perfect.&lt;/li&gt;
&lt;li&gt;New insert with &lt;code&gt;created_at &amp;lt; cursor&lt;/code&gt; — same story; the new row is &lt;em&gt;in front of&lt;/em&gt; the cursor and thus in future forward pages.&lt;/li&gt;
&lt;li&gt;Delete a row already paginated — its removal doesn't affect anything; the cursor points to a row that's still in the ordered sequence (or, if the cursor itself is deleted, the next call skips ahead to the next valid row).&lt;/li&gt;
&lt;li&gt;Update the paginated sort column (e.g. &lt;code&gt;UPDATE events SET created_at = ...&lt;/code&gt;) — the updated row can move in the sequence, but since keyset uses the &lt;em&gt;cursor's remembered tuple&lt;/em&gt;, not the current row's tuple, subsequent pages remain consistent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common newbie mistakes.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Using per-column WHERE instead of tuple comparison — &lt;code&gt;WHERE created_at &amp;lt; :ts AND id &amp;lt; :id&lt;/code&gt; drops rows where &lt;code&gt;created_at &amp;lt; :ts AND id ≥ :id&lt;/code&gt;. Wrong semantically.&lt;/li&gt;
&lt;li&gt;Forgetting the tie-breaker — two rows with the same &lt;code&gt;created_at&lt;/code&gt; alternate between pages.&lt;/li&gt;
&lt;li&gt;Wrong ORDER BY direction — &lt;code&gt;ORDER BY created_at DESC, id ASC&lt;/code&gt; with a &lt;code&gt;&amp;lt;&lt;/code&gt; comparison drops the wrong rows.&lt;/li&gt;
&lt;li&gt;Missing composite index — the query works but plans as a Filter after a full sort. Latency stays O(n).&lt;/li&gt;
&lt;li&gt;Encoding the cursor as &lt;code&gt;page=N&lt;/code&gt; — this is not keyset. Cursors are opaque tuples, not page numbers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Interview probes on syntax fluency.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Why is tuple comparison better than per-column AND/OR?"&lt;/strong&gt; — one-shot lexicographic comparison, single index-range scan, no chance of dropping edge rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"What index does keyset need?"&lt;/strong&gt; — composite on the ORDER BY columns in the ORDER BY direction, matching direction to the DESC/ASC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"What if two rows share a millisecond?"&lt;/strong&gt; — the unique tie-breaker (&lt;code&gt;id&lt;/code&gt;) disambiguates. Every keyset paginator ends the ORDER BY with &lt;code&gt;, id&lt;/code&gt; or another unique column.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"What if the sort column is NULL for some rows?"&lt;/strong&gt; — either the column is &lt;code&gt;NOT NULL&lt;/code&gt;, or you use &lt;code&gt;COALESCE(sort_col, sentinel)&lt;/code&gt; in both the WHERE and ORDER BY. The NULL problem is one of the two most common keyset bugs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"How do you paginate backward?"&lt;/strong&gt; — swap &lt;code&gt;&amp;lt;&lt;/code&gt; for &lt;code&gt;&amp;gt;&lt;/code&gt;, flip the ORDER BY direction, reverse the result in application code. Or use &lt;code&gt;first/last + after/before&lt;/code&gt; if you're implementing the Relay spec.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — the "next 20 events" keyset query on Postgres
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The canonical keyset query — DESC pagination on &lt;code&gt;(created_at, id)&lt;/code&gt; with a composite index. Every consumer feed on the internet uses this shape.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given &lt;code&gt;events(id BIGINT PRIMARY KEY, created_at TIMESTAMPTZ NOT NULL, user_id BIGINT, payload TEXT)&lt;/code&gt; with an index on &lt;code&gt;(created_at DESC, id DESC)&lt;/code&gt;, write the Postgres query that returns the next 20 events after a cursor &lt;code&gt;(cursor_ts, cursor_id)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;10M rows, sorted DESC by &lt;code&gt;(created_at, id)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Cursor is &lt;code&gt;(2026-07-05 12:00, 5000000)&lt;/code&gt; — the last row of the previous page.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(:&lt;/span&gt;&lt;span class="n"&gt;cursor_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;cursor_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;WHERE (created_at, id) &amp;lt; (:cursor_ts, :cursor_id)&lt;/code&gt; — tuple comparison. Lexicographic — first &lt;code&gt;created_at &amp;lt; :cursor_ts&lt;/code&gt;, or &lt;code&gt;created_at = :cursor_ts AND id &amp;lt; :cursor_id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;This maps to a range predicate on the composite index &lt;code&gt;(created_at DESC, id DESC)&lt;/code&gt;. Postgres does one index seek to position at the cursor, then scans backward.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ORDER BY created_at DESC, id DESC&lt;/code&gt; — matches the index order. No sort step.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LIMIT 20&lt;/code&gt; — bounds the scan. The engine reads exactly 20 rows past the cursor position and stops.&lt;/li&gt;
&lt;li&gt;Total plan cost — &lt;code&gt;Index Scan Backward using idx_events_paginate  (cost=0.43..1.28)&lt;/code&gt; with &lt;code&gt;actual rows=20 actual loops=1&lt;/code&gt;. Flat regardless of pagination depth.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output&lt;/strong&gt; (20 rows just older than the cursor).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;created_at&lt;/th&gt;
&lt;th&gt;payload&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;4999999&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;2026-07-05 11:59:59&lt;/td&gt;
&lt;td&gt;click&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4999998&lt;/td&gt;
&lt;td&gt;91&lt;/td&gt;
&lt;td&gt;2026-07-05 11:59:58&lt;/td&gt;
&lt;td&gt;login&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4999980&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;2026-07-05 11:59:00&lt;/td&gt;
&lt;td&gt;logout&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; The two-line skeleton — tuple compare + ORDER BY DESC — is the entire keyset primitive. Memorise both slots; every future keyset query is a variation on this template.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — expanded per-column form for SQL Server (no row-value comparison)
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; SQL Server (and older MySQL) doesn't support the ANSI row-value comparison &lt;code&gt;WHERE (a, b) &amp;lt; (:a, :b)&lt;/code&gt;. The expanded form is verbose but semantically identical.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Rewrite the previous keyset query using per-column WHERE clauses for SQL Server compatibility. Verify the planner still uses a range scan.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt; (Same events table, on SQL Server.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- SQL Server — expanded per-column form&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;TOP&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
     &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;cursor_ts&lt;/span&gt;
  &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;cursor_ts&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;cursor_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Row-value comparison &lt;code&gt;(a, b) &amp;lt; (:a, :b)&lt;/code&gt; is &lt;em&gt;not&lt;/em&gt; SQL Server-native. The expanded form is &lt;code&gt;a &amp;lt; :a OR (a = :a AND b &amp;lt; :b)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;OR&lt;/code&gt; decomposition is critical — &lt;code&gt;a &amp;lt; :a AND b &amp;lt; :b&lt;/code&gt; is &lt;em&gt;wrong&lt;/em&gt; — it drops rows where &lt;code&gt;a &amp;lt; :a AND b &amp;gt;= :b&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;SQL Server's optimiser recognises the &lt;code&gt;OR (equal AND less)&lt;/code&gt; shape and typically compiles it to a range seek on the composite index. Verify with the execution plan.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SELECT TOP 20&lt;/code&gt; replaces &lt;code&gt;LIMIT 20&lt;/code&gt; — SQL Server's spelling. Semantically identical.&lt;/li&gt;
&lt;li&gt;If the planner does &lt;em&gt;not&lt;/em&gt; recognise the pattern (older SQL Server versions, or complex predicates), it may fall back to a scan + filter — always verify with the actual plan.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt; Same 20 rows as the tuple-form query.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; On SQL Server and older MySQL, use the expanded &lt;code&gt;OR (equal AND less)&lt;/code&gt; form. It's more verbose but semantically identical. Every keyset paginator against SQL Server ships this pattern.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — backward pagination (previous page)
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The Relay spec's &lt;code&gt;last + before&lt;/code&gt; — "give me the 20 rows immediately newer than this cursor." Implementable with a flipped tuple compare and a reversed result set.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write a Postgres query that returns the 20 events immediately &lt;em&gt;newer&lt;/em&gt; than a cursor &lt;code&gt;(cursor_ts, cursor_id)&lt;/code&gt;, in DESC order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt; (Same events table; cursor &lt;code&gt;(2026-07-05 12:00, 5000000)&lt;/code&gt;.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Backward keyset — get the 20 rows immediately newer than the cursor&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(:&lt;/span&gt;&lt;span class="n"&gt;cursor_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;cursor_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;
  &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Inner query — flip &lt;code&gt;&amp;lt;&lt;/code&gt; to &lt;code&gt;&amp;gt;&lt;/code&gt; (newer, not older) and flip ORDER BY to ASC (so the closest rows to the cursor are read first). LIMIT 20.&lt;/li&gt;
&lt;li&gt;This produces 20 rows in ASC order — the "reading direction" for backward pagination but the wrong final order for the UI.&lt;/li&gt;
&lt;li&gt;Outer query — re-sort DESC to match the standard feed ordering. Rows are the same; the presentation flips.&lt;/li&gt;
&lt;li&gt;The two-step (inner-ASC, outer-DESC) is unavoidable — you need the closest-first read direction internally but the newest-first presentation externally.&lt;/li&gt;
&lt;li&gt;On engines with &lt;code&gt;FETCH FIRST 20 ROWS ONLY&lt;/code&gt; inside a subquery restriction (Oracle 12c+), the same pattern works with the ANSI spelling instead of &lt;code&gt;LIMIT&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output&lt;/strong&gt; (20 rows just newer than the cursor, presented newest-first).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;created_at&lt;/th&gt;
&lt;th&gt;payload&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5000020&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;2026-07-05 12:00:20&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5000001&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;2026-07-05 12:00:01&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Backward pagination flips the operator, flips the direction, and reverses the result. The composite index on &lt;code&gt;(created_at DESC, id DESC)&lt;/code&gt; works for both directions on Postgres 15+.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — keyset over composite ORDER BY with mixed directions
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A subtle case — the ORDER BY has one DESC column and one ASC tie-breaker (&lt;code&gt;ORDER BY score DESC, id ASC&lt;/code&gt; for leaderboards). The tuple comparison needs to handle the mixed direction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write a Postgres keyset query for a leaderboard sorted by &lt;code&gt;score DESC, id ASC&lt;/code&gt;, given a cursor &lt;code&gt;(cursor_score, cursor_id)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt; &lt;code&gt;leaderboard(id INT PRIMARY KEY, score INT NOT NULL)&lt;/code&gt;. Composite index &lt;code&gt;(score DESC, id ASC)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Leaderboard: score DESC, id ASC as tie-breaker&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;leaderboard&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
     &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;cursor_score&lt;/span&gt;
  &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;cursor_score&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;cursor_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The ORDER BY has mixed directions — &lt;code&gt;score DESC, id ASC&lt;/code&gt;. Row-value comparison doesn't fit — &lt;code&gt;(score, id) &amp;lt; (:cursor_score, :cursor_id)&lt;/code&gt; would compare both columns in the same direction.&lt;/li&gt;
&lt;li&gt;Use the expanded form — &lt;code&gt;score &amp;lt; :cursor_score&lt;/code&gt; (strictly worse score) &lt;code&gt;OR (score = :cursor_score AND id &amp;gt; :cursor_id)&lt;/code&gt; (same score, larger id — the tie-breaker direction).&lt;/li&gt;
&lt;li&gt;The composite index must match — &lt;code&gt;(score DESC, id ASC)&lt;/code&gt;. Postgres supports mixed-direction indexes since 8.3.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;OR (score = :cursor_score AND id &amp;gt; :cursor_id)&lt;/code&gt; — note &lt;code&gt;&amp;gt;&lt;/code&gt;, not &lt;code&gt;&amp;lt;&lt;/code&gt; — because the tie-breaker is ASC.&lt;/li&gt;
&lt;li&gt;Interviewers love this — a candidate who reflexively types &lt;code&gt;(score, id) &amp;lt; (:cs, :ci)&lt;/code&gt; shows they haven't thought through the mixed-direction case.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output&lt;/strong&gt; (20 rows just past the cursor in the leaderboard).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;td&gt;4800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;102&lt;/td&gt;
&lt;td&gt;4800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;55&lt;/td&gt;
&lt;td&gt;4795&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Mixed-direction ORDER BY needs the expanded &lt;code&gt;OR (equal AND direction-specific)&lt;/code&gt; form. Row-value comparison only works when &lt;em&gt;all&lt;/em&gt; columns share the same direction. Every leaderboard paginator ships this pattern.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on keyset design
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Design the composite index and the SQL for a keyset-paginated &lt;code&gt;chat_messages&lt;/code&gt; endpoint that returns the last 30 messages of a specific &lt;code&gt;channel_id&lt;/code&gt;. Explain how the index alignment guarantees O(log n) at any depth."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using channel-scoped composite index + keyset tuple compare
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Table&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;chat_messages&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;           &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;channel_id&lt;/span&gt;   &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt;   &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt;      &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;body&lt;/span&gt;         &lt;span class="nb"&gt;TEXT&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Channel-scoped composite index&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_chat_channel_paginate&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;chat_messages&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Page 1: last 30 messages of channel 42&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;chat_messages&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;channel_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;channel_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Page 2: next 30 older messages after the cursor (last row of page 1)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;chat_messages&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;channel_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;channel_id&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(:&lt;/span&gt;&lt;span class="n"&gt;cursor_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;cursor_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Composite index &lt;code&gt;(channel_id, created_at DESC, id DESC)&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Channel as prefix; sort keys after&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Page 1 query hits index at &lt;code&gt;channel_id = 42&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;O(log n) seek to channel prefix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Reads backward from first leaf&lt;/td&gt;
&lt;td&gt;O(30) sequential reads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Page 2 adds tuple predicate&lt;/td&gt;
&lt;td&gt;Positions at cursor within channel prefix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Reads next 30 rows backward&lt;/td&gt;
&lt;td&gt;O(30) more sequential reads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Cursor is opaque &lt;code&gt;base64((created_at, id))&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Server issues; client passes back&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The composite index has &lt;code&gt;channel_id&lt;/code&gt; as its leading column so every message in a given channel lives in a contiguous range of the index. Within that range, rows are ordered &lt;code&gt;(created_at DESC, id DESC)&lt;/code&gt;, so both the initial page and every subsequent keyset page hit the same tight index range. Plan is &lt;code&gt;Index Scan Backward&lt;/code&gt; with &lt;code&gt;actual rows = 30 actual loops = 1&lt;/code&gt; for both pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Page 1&lt;/th&gt;
&lt;th&gt;Page 100&lt;/th&gt;
&lt;th&gt;Page 10,000&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Plan&lt;/td&gt;
&lt;td&gt;Index Scan Backward&lt;/td&gt;
&lt;td&gt;Index Scan Backward&lt;/td&gt;
&lt;td&gt;Index Scan Backward&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rows scanned&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latency (10M msgs, 10K channels)&lt;/td&gt;
&lt;td&gt;4 ms&lt;/td&gt;
&lt;td&gt;4 ms&lt;/td&gt;
&lt;td&gt;4 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cursor size (base64)&lt;/td&gt;
&lt;td&gt;~48 B&lt;/td&gt;
&lt;td&gt;~48 B&lt;/td&gt;
&lt;td&gt;~48 B&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Composite index with channel_id as prefix&lt;/strong&gt;&lt;/strong&gt; — the leading column matches the WHERE filter (&lt;code&gt;channel_id = :cid&lt;/code&gt;), so the index seek positions at the start of the channel's rows in &lt;code&gt;O(log n)&lt;/code&gt;. Everything after that is a sequential index-leaf scan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;ORDER BY matches the composite index&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;(created_at DESC, id DESC)&lt;/code&gt; inside the channel prefix is stored contiguously; Postgres reads backward with no sort node.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Tuple compare uses the same index range&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;AND (created_at, id) &amp;lt; (:cursor_ts, :cursor_id)&lt;/code&gt; is a range predicate on the composite; the planner tightens the initial seek by one more equality-then-less-than step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Flat latency at any pagination depth&lt;/strong&gt;&lt;/strong&gt; — every page hits the exact same plan shape. Latency at page 1 = latency at page 10,000 = latency at page 100,000.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cursor size is bounded&lt;/strong&gt;&lt;/strong&gt; — a &lt;code&gt;(TIMESTAMPTZ, BIGINT)&lt;/code&gt; tuple base64-encodes to ~48 bytes. Fits in a URL query parameter without percent-encoding overhead. HMAC-signed it grows to ~80 bytes — still fine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;O(log n + K)&lt;/code&gt; per page where &lt;code&gt;n&lt;/code&gt; is the total row count and &lt;code&gt;K&lt;/code&gt; is the LIMIT. For &lt;code&gt;n = 10M&lt;/code&gt; and &lt;code&gt;K = 30&lt;/code&gt;, that's ~24 index-leaf reads (log₂(10M) ≈ 24) plus 30 sequential reads — well under 5 ms on any modern hardware.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — indexing&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Composite indexing drills&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/indexing" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — top-N per group&lt;/span&gt;
&lt;strong&gt;Top-N per group problems&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/top-n" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  5. Cursor pagination + dialect matrix
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;sql cursor pagination&lt;/code&gt; and &lt;code&gt;sql infinite scroll&lt;/code&gt; — opaque base64 tokens, HMAC signing, the GraphQL Relay spec, and the six-engine dialect matrix that ties it all together
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;&lt;code&gt;sql cursor pagination&lt;/code&gt; is the API-shape layer on top of keyset — the client sends an opaque &lt;code&gt;after&lt;/code&gt; token, the server decodes it into &lt;code&gt;(cursor_col, cursor_id)&lt;/code&gt;, runs the keyset SQL, and returns the next page plus an &lt;code&gt;endCursor&lt;/code&gt; for the client to send on the next call, and the whole family of &lt;code&gt;sql infinite scroll&lt;/code&gt; endpoints (REST, GraphQL Relay, gRPC streaming) reduces to this one wire contract with slightly different naming&lt;/strong&gt;. Once you nail the opaque-token discipline and the Relay &lt;code&gt;edges/pageInfo&lt;/code&gt; shape, every consumer feed is a variation on the same theme.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3zro3dd9u4bb15ofasfk.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3zro3dd9u4bb15ofasfk.jpeg" alt="Visual diagram of cursor pagination and the dialect matrix — left an opaque base64 cursor token card decoding to a JSON payload of ts/id/dir, centre a GraphQL Relay edges/pageInfo card, right a six-column dialect matrix marking each engine as green (keyset works well) / amber (works, watch NULL ordering) / red (OFFSET-only in practice) plus a gotchas annotation strip; on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Slot 1 — the opaque cursor token.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cursors are &lt;strong&gt;opaque&lt;/strong&gt; to clients — a base64-encoded string, not a page number, not a raw tuple. Clients treat cursors as black boxes; the server owns the encoding.&lt;/li&gt;
&lt;li&gt;Payload — usually a JSON blob &lt;code&gt;{"ts": "2026-07-05T12:00:00Z", "id": 5000000, "dir": "desc"}&lt;/code&gt; or a msgpack / protobuf equivalent. JSON is simplest; msgpack shrinks the on-the-wire size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HMAC-signed.&lt;/strong&gt; Every cursor is signed with a server-side secret so a client can't tamper with it to skip rows or leak data. &lt;code&gt;cursor = base64(json_payload).hmac(secret)&lt;/code&gt;. Verification is O(1) on the way in.&lt;/li&gt;
&lt;li&gt;Encoding — &lt;code&gt;base64url&lt;/code&gt; (no &lt;code&gt;+/&lt;/code&gt; padding characters) is URL-safe and drop-in for query parameters.&lt;/li&gt;
&lt;li&gt;Rotation — the HMAC secret rotates every N months; cursors older than the rotation window return a 400 with a hint to re-fetch page 1.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 2 — the Relay Connections spec.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GraphQL's Relay spec (2015, still the standard) defines cursor pagination as &lt;code&gt;first / after / last / before&lt;/code&gt; with &lt;code&gt;edges&lt;/code&gt; and &lt;code&gt;pageInfo&lt;/code&gt; envelope.&lt;/li&gt;
&lt;li&gt;Query — &lt;code&gt;orders(first: 20, after: "eyJ0cyI6...").&lt;/code&gt; &lt;code&gt;edges { node { id, ...fields }, cursor } pageInfo { hasNextPage, endCursor }&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Every edge has its own &lt;code&gt;cursor&lt;/code&gt; (usable as the &lt;code&gt;after&lt;/code&gt; for a "page start here" request). The &lt;code&gt;endCursor&lt;/code&gt; is a shorthand for the last edge's cursor.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hasNextPage&lt;/code&gt; — cheap via the LIMIT n+1 peek-ahead trick. Fetch 21 rows, return 20, &lt;code&gt;hasNext = (fetched &amp;gt; 20)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hasPreviousPage&lt;/code&gt; — same trick backward, or &lt;code&gt;hasPrev = (after cursor is set)&lt;/code&gt;. Not always cheap to compute exactly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 3 — REST cursor endpoints.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The naming varies — Twitter uses &lt;code&gt;cursor&lt;/code&gt;, GitHub uses &lt;code&gt;page&lt;/code&gt;+&lt;code&gt;per_page&lt;/code&gt; with &lt;code&gt;Link&lt;/code&gt; headers, Stripe uses &lt;code&gt;starting_after&lt;/code&gt; / &lt;code&gt;ending_before&lt;/code&gt;. All are cursor-based under the hood.&lt;/li&gt;
&lt;li&gt;Response envelope — &lt;code&gt;{"data": [...], "next_cursor": "eyJ0cyI6...", "prev_cursor": null}&lt;/code&gt;. The client passes &lt;code&gt;next_cursor&lt;/code&gt; on the next call.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Link&lt;/code&gt; header (GitHub-style) — RFC 5988 links: &lt;code&gt;&amp;lt;https://api.example.com/orders?cursor=eyJ...&amp;gt;; rel="next", &amp;lt;...&amp;gt;; rel="prev"&lt;/code&gt;. Elegant; clients can parse without knowing the JSON envelope.&lt;/li&gt;
&lt;li&gt;Idempotency — the same cursor + limit returns the same rows (assuming no schema changes). Clients can retry safely.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 4 — cursor lifecycle and invalidation.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cursors survive &lt;strong&gt;inserts&lt;/strong&gt; — they point to a specific row, so new rows in the ordered stream don't affect the cursor's target.&lt;/li&gt;
&lt;li&gt;Cursors survive &lt;strong&gt;deletes&lt;/strong&gt; — if the cursor's target row is deleted, the next call reads from just past the (now missing) target position. Effectively the cursor still works.&lt;/li&gt;
&lt;li&gt;Cursors survive &lt;strong&gt;schema changes&lt;/strong&gt; — as long as the sort columns are unchanged. If the sort column changes semantics (e.g. renaming &lt;code&gt;created_at&lt;/code&gt; to &lt;code&gt;posted_at&lt;/code&gt;), old cursors become invalid and the server should reject them with a clear error.&lt;/li&gt;
&lt;li&gt;Cursors survive &lt;strong&gt;secret rotation&lt;/strong&gt; — with graceful degradation. Rotate secrets; grandfather the previous secret for one rotation window; reject cursors older than that.&lt;/li&gt;
&lt;li&gt;Cursors are &lt;strong&gt;short-lived&lt;/strong&gt; in practice — clients drop them on tab close. But an idle browser tab holding a cursor for a week should still work if the schema hasn't changed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 5 — the six-engine dialect matrix for keyset.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Postgres&lt;/strong&gt; — full support. &lt;code&gt;(a, b) &amp;lt; (:a, :b)&lt;/code&gt; tuple compare + composite btree in matching direction. Best case.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MySQL 8&lt;/strong&gt; — full support since 8.0.16 for row-value comparison; also supports descending indexes since 8.0.13. Perfect for keyset.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL Server&lt;/strong&gt; — no row-value comparison; use expanded &lt;code&gt;OR (equal AND less)&lt;/code&gt; form. Composite index seek works via query planner recognition; verify with the plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Oracle 12c+&lt;/strong&gt; — supports row-value comparison and composite descending indexes. Same story as Postgres.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snowflake&lt;/strong&gt; — supports row-value comparison. No btree indexes; the equivalent is &lt;code&gt;CLUSTER BY (sort_col)&lt;/code&gt; for micro-partition co-location. Deep-page latency depends on cluster health.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BigQuery&lt;/strong&gt; — supports row-value comparison in &lt;code&gt;WHERE&lt;/code&gt;. No btree; the equivalent is table &lt;code&gt;CLUSTER BY sort_col&lt;/code&gt; plus partition pruning if the sort_col has a date component. Best practice: partition by DATE(sort_col), cluster by sort_col, then keyset.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 6 — dialect gotchas.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;NULL ordering.&lt;/strong&gt; Postgres and Oracle default to NULLs first for DESC; MySQL and SQL Server default to NULLs last; BigQuery defaults to NULLs first. Always spell out &lt;code&gt;NULLS LAST&lt;/code&gt; explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Millisecond precision.&lt;/strong&gt; Different engines store &lt;code&gt;TIMESTAMP&lt;/code&gt; at different resolutions — Postgres microseconds, MySQL up to microseconds, SQL Server 100-ns, BigQuery microseconds. Two "identical" timestamps in one engine may differ in another — always include the &lt;code&gt;id&lt;/code&gt; tie-breaker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Case sensitivity in text sort columns.&lt;/strong&gt; &lt;code&gt;created_at&lt;/code&gt; is numeric so no problem; a &lt;code&gt;name&lt;/code&gt;-based sort has to consider collation. &lt;code&gt;ORDER BY name COLLATE "C"&lt;/code&gt; or the equivalent, plus include the collation in the cursor to avoid inconsistency across mixed locale servers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Composite index size.&lt;/strong&gt; A composite &lt;code&gt;(a, b, c)&lt;/code&gt; index is 30–50% larger than a single-column &lt;code&gt;(a)&lt;/code&gt; index. Keyset requires the composite; the storage cost is the price of admission. Usually worth it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Row-value comparison support.&lt;/strong&gt; SQL Server, older MySQL, and some BigQuery legacy modes don't recognise &lt;code&gt;(a, b) &amp;lt; (:a, :b)&lt;/code&gt; as a row-value comparison — use the expanded form.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 7 — the peek-ahead trick for &lt;code&gt;hasNextPage&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Client asks for &lt;code&gt;first: 20&lt;/code&gt;. Server queries with &lt;code&gt;LIMIT 21&lt;/code&gt; (one extra). If the query returns 21 rows, &lt;code&gt;hasNextPage = true&lt;/code&gt; and the server returns only the first 20; if it returns ≤ 20, &lt;code&gt;hasNextPage = false&lt;/code&gt; and the server returns all of them.&lt;/li&gt;
&lt;li&gt;Zero-cost — the 21st row is a single extra index leaf read. No separate &lt;code&gt;SELECT COUNT(*)&lt;/code&gt; query.&lt;/li&gt;
&lt;li&gt;The 21st row's cursor is the &lt;code&gt;endCursor&lt;/code&gt; for the &lt;em&gt;next&lt;/em&gt; page's &lt;code&gt;after&lt;/code&gt; parameter — perfect handoff.&lt;/li&gt;
&lt;li&gt;Every Relay implementation ships this trick. If your endpoint doesn't do it, &lt;code&gt;hasNextPage&lt;/code&gt; requires a separate query that's expensive on large tables.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 8 — HMAC signing and secret management.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Server has a secret &lt;code&gt;S&lt;/code&gt;. Cursor payload is &lt;code&gt;P = json({"ts": ..., "id": ...})&lt;/code&gt;. Signed cursor is &lt;code&gt;base64url(P) + "." + hmac_sha256(S, base64url(P))&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;On the way in — verify signature first. If invalid, return 400 with "invalid cursor." No SQL runs.&lt;/li&gt;
&lt;li&gt;Secrets live in a KMS / secret manager. Rotate every 90 days. On rotation, keep the previous secret valid for a grace period equal to the maximum expected client idle time (usually 24–72 hours).&lt;/li&gt;
&lt;li&gt;Alternative — encrypt the payload symmetrically with the secret. Adds tamper-proofing plus payload privacy. Not strictly needed for pagination (the payload is not confidential), but common in enterprise deployments.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 9 — total count and its trade-offs.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keyset gives you &lt;code&gt;hasNextPage&lt;/code&gt; cheaply; it does &lt;em&gt;not&lt;/em&gt; give you &lt;code&gt;totalCount&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Exact total on a large mutable table is &lt;code&gt;O(n)&lt;/code&gt; regardless — &lt;code&gt;SELECT COUNT(*)&lt;/code&gt; scans the whole thing or the index.&lt;/li&gt;
&lt;li&gt;Approximations — &lt;code&gt;pg_class.reltuples&lt;/code&gt; on Postgres, &lt;code&gt;information_schema.tables.table_rows&lt;/code&gt; on MySQL, &lt;code&gt;SYSTEM_SAMPLING&lt;/code&gt; on Snowflake. Fast but stale.&lt;/li&gt;
&lt;li&gt;Best practice — don't return total count on infinite-scroll feeds. Show a scroll bar that grows dynamically instead. On admin dashboards, cache the total count and refresh every few minutes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 10 — the interview litmus test.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Design a paginated timeline endpoint." Every good answer starts with cursor pagination, keyset SQL, opaque tokens, and the Relay spec. If the candidate reaches for &lt;code&gt;page=1&amp;amp;limit=20&lt;/code&gt;, they're a junior.&lt;/li&gt;
&lt;li&gt;"How do you handle 'jump to page 47'?" — you don't, on infinite scroll. On admin dashboards, use capped OFFSET; on data exports, iterate keyset from page 1.&lt;/li&gt;
&lt;li&gt;"Explain hasNextPage without an extra query." — the LIMIT n+1 peek-ahead. Instant senior signal.&lt;/li&gt;
&lt;li&gt;"Explain why cursors are opaque." — tamper resistance, forward compatibility (server can change the payload schema without breaking clients), and abstraction (clients shouldn't reason about database internals).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common newbie mistakes.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Exposing raw tuples in cursors. &lt;code&gt;?cursor=2026-07-05T12:00:00Z_5000000&lt;/code&gt; is a leaky abstraction and a tampering vector.&lt;/li&gt;
&lt;li&gt;Skipping HMAC. Anyone can craft a cursor to read rows they shouldn't see. Sign every cursor.&lt;/li&gt;
&lt;li&gt;Assuming keyset gives you &lt;code&gt;totalCount&lt;/code&gt;. It doesn't; that's a separate expensive query.&lt;/li&gt;
&lt;li&gt;Not planning for schema changes. When the sort column changes semantics, old cursors must be rejected cleanly.&lt;/li&gt;
&lt;li&gt;Using page numbers instead of cursors on a "list this user's posts" endpoint. If two viewers see slightly different data (privacy filters), page numbers don't align across viewers.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — encoding and decoding an opaque cursor with HMAC
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The full round-trip — encode a &lt;code&gt;(created_at, id)&lt;/code&gt; tuple into a base64 HMAC-signed cursor, decode it on the way in with signature verification. Every consumer feed does this exactly once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write Python that encodes a cursor from &lt;code&gt;(created_at, id)&lt;/code&gt; with HMAC-SHA256 signing, and decodes it back on the way in with signature verification. Show the wire format.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt; &lt;code&gt;created_at = datetime(2026, 7, 5, 12, 0)&lt;/code&gt;, &lt;code&gt;id = 5000000&lt;/code&gt;, secret = &lt;code&gt;b"pagination-secret-2026"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;


&lt;span class="n"&gt;SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pagination-secret-2026&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;encode_cursor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;astimezone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dir&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;desc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;payload_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;separators&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;sort_keys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;payload_b64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlsafe_b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload_bytes&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload_b64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;sig_b64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlsafe_b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;payload_b64&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sig_b64&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;decode_cursor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;payload_b64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sig_b64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;malformed cursor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Verify signature
&lt;/span&gt;    &lt;span class="n"&gt;expected_sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload_b64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;expected_sig_b64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlsafe_b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected_sig&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compare_digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sig_b64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected_sig_b64&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cursor signature invalid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Decode payload
&lt;/span&gt;    &lt;span class="n"&gt;padding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload_b64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;payload_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlsafe_b64decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload_b64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromisoformat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="c1"&gt;# Round-trip
&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tzinfo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;encode_cursor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5_000_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# eyJkaXIiOiJkZXNjIiwiaWQiOjUwMDAwMDAsInRzIjoiMjAyNi0wNy0wNVQxMjowMDowMCswMDowMCJ9.CzRK9pKu1EWvKQrKn2H8kMXvpQXwZR7Tu-y1O4iF1Ho
&lt;/span&gt;
&lt;span class="n"&gt;ts2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decode_cursor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;assert &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5_000_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Payload — a JSON dict with &lt;code&gt;ts&lt;/code&gt;, &lt;code&gt;id&lt;/code&gt;, and &lt;code&gt;dir&lt;/code&gt;. Sort keys for determinism (so re-encoding produces the same bytes).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;base64.urlsafe_b64encode&lt;/code&gt; — URL-safe base64 (uses &lt;code&gt;-_&lt;/code&gt; instead of &lt;code&gt;+/&lt;/code&gt;), strips &lt;code&gt;=&lt;/code&gt; padding so the cursor is drop-in for URL query parameters.&lt;/li&gt;
&lt;li&gt;Signature — HMAC-SHA256 over the &lt;em&gt;base64&lt;/em&gt; payload (not the raw JSON, so the signature covers the exact wire form). Base64-encode the signature too.&lt;/li&gt;
&lt;li&gt;Wire format — &lt;code&gt;payload_b64.signature_b64&lt;/code&gt;. Two base64url strings separated by a dot, JWT-like but without a header.&lt;/li&gt;
&lt;li&gt;Decode — split on the dot, verify signature with &lt;code&gt;hmac.compare_digest&lt;/code&gt; (constant-time compare to prevent timing attacks), then decode the payload.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cursor &lt;code&gt;eyJkaXIiOiJkZXNjIiwiaWQiOjUwMDAwMDAsInRzIjoiMjAyNi0wNy0wNVQxMjowMDowMCswMDowMCJ9.CzRK9pKu1EWvKQrKn2H8kMXvpQXwZR7Tu-y1O4iF1Ho&lt;/code&gt; (or similar; signature varies with the secret).&lt;/li&gt;
&lt;li&gt;Decode returns &lt;code&gt;(datetime(2026,7,5,12,0,tzinfo=UTC), 5_000_000)&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; JWT-like signed cursors are the industry standard. Every consumer API in 2026 uses this exact shape (with minor variations in payload schema and signing algorithm). Roll it yourself in 30 lines of code or use a library like &lt;code&gt;itsdangerous&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the Relay Connections envelope
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The GraphQL Relay spec, in one worked query. This is what every &lt;code&gt;graphql-ruby&lt;/code&gt;, &lt;code&gt;graphene&lt;/code&gt;, &lt;code&gt;strawberry&lt;/code&gt;, and &lt;code&gt;apollo-server&lt;/code&gt; codebase writes verbatim.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given the events table and the keyset SQL from before, write the resolver for a Relay-style &lt;code&gt;events(first: 20, after: "...")&lt;/code&gt; connection query.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt; &lt;code&gt;events(id, user_id, created_at, payload)&lt;/code&gt; table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;resolve_events_connection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;after&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Clamp first to prevent unbounded page sizes
&lt;/span&gt;    &lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Decode the after cursor (if any)
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;after&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;cursor_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cursor_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decode_cursor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;GraphQLError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invalid cursor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            SELECT id, user_id, created_at, payload
            FROM events
            WHERE (created_at, id) &amp;lt; (%s, %s)
            ORDER BY created_at DESC, id DESC
            LIMIT %s
            &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cursor_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cursor_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            SELECT id, user_id, created_at, payload
            FROM events
            ORDER BY created_at DESC, id DESC
            LIMIT %s
            &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;has_next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;edges&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;node&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;created_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;created_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cursor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;encode_cursor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;created_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;edges&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;edges&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pageInfo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hasNextPage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;has_next&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hasPreviousPage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;after&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;startCursor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;edges&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cursor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;edges&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;endCursor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;edges&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cursor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;edges&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Clamp &lt;code&gt;first&lt;/code&gt; to a safe range (1–100). Prevents a client from asking for a million-row page.&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;after&lt;/code&gt; is provided, decode it (with signature verification). If invalid, return a GraphQL error.&lt;/li&gt;
&lt;li&gt;SQL — keyset if &lt;code&gt;after&lt;/code&gt; is set, initial-page if not. Both queries fetch &lt;code&gt;first + 1&lt;/code&gt; rows for the peek-ahead.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;has_next = (len(rows) &amp;gt; first)&lt;/code&gt; — the peek-ahead trick. Trim to &lt;code&gt;first&lt;/code&gt; rows before returning.&lt;/li&gt;
&lt;li&gt;Build &lt;code&gt;edges&lt;/code&gt; with a per-row &lt;code&gt;cursor&lt;/code&gt; (generated from &lt;code&gt;(created_at, id)&lt;/code&gt;) plus &lt;code&gt;pageInfo&lt;/code&gt; with &lt;code&gt;hasNextPage&lt;/code&gt; / &lt;code&gt;hasPreviousPage&lt;/code&gt; / &lt;code&gt;startCursor&lt;/code&gt; / &lt;code&gt;endCursor&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output&lt;/strong&gt; — Relay-shaped JSON envelope, ready for &lt;code&gt;apollo-client&lt;/code&gt; or &lt;code&gt;relay-runtime&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; The &lt;code&gt;first + 1&lt;/code&gt; peek-ahead + &lt;code&gt;edges + pageInfo&lt;/code&gt; envelope is the entire Relay spec, condensed. Every GraphQL feed on the internet ships this exact pattern.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — BigQuery keyset with partition pruning
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The BigQuery specialisation — partition by DATE(sort_col) and cluster by sort_col. Keyset works, but you have to be careful with the outer WHERE to prune partitions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Design the BigQuery keyset paginator for a 10B-row events table, partitioned by DATE(created_at) and clustered by (created_at, id). Include the partition prune predicate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt; &lt;code&gt;events&lt;/code&gt; table on BigQuery with &lt;code&gt;PARTITION BY DATE(created_at) CLUSTER BY created_at, id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- BigQuery keyset with partition prune&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;`project.dataset.events`&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
  &lt;span class="c1"&gt;-- Partition prune: only relevant days&lt;/span&gt;
  &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt;
    &lt;span class="n"&gt;DATE_SUB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;cursor_ts&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;cursor_ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;-- Keyset tuple comparison&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;cursor_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;cursor_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Partition prune — &lt;code&gt;DATE(created_at) BETWEEN cursor_date - 7 AND cursor_date&lt;/code&gt;. Reads only the last 7 days of partitions relative to the cursor. Tighter windows read fewer partitions but risk missing older rows if the user paginates deep.&lt;/li&gt;
&lt;li&gt;Alternative — &lt;code&gt;DATE(created_at) &amp;lt;= DATE(@cursor_ts)&lt;/code&gt;. Prunes all partitions newer than the cursor; reads all older ones. Broader but always correct.&lt;/li&gt;
&lt;li&gt;Keyset tuple compare — same as Postgres/MySQL. BigQuery has supported row-value comparison since 2019.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CLUSTER BY (created_at, id)&lt;/code&gt; — within each partition, rows are physically co-located by &lt;code&gt;(created_at, id)&lt;/code&gt;. The tuple compare uses the clustering to prune blocks.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LIMIT 21&lt;/code&gt; — peek-ahead for &lt;code&gt;hasNextPage&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt; 20 rows per page, flat latency at any depth, ~150 ms per page on a 10B-row table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; On BigQuery (or any partitioned columnar warehouse), always combine keyset with a partition-prune predicate. Skipping the prune predicate means a full-table scan even with keyset — the tuple compare alone won't prune partitions on its own.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on API-shape and cursor discipline
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Design the wire format for an infinite-scroll &lt;code&gt;messages&lt;/code&gt; endpoint. What goes in the cursor, how do you sign it, what does the response envelope look like, and how do you handle secret rotation?"&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using signed opaque cursor + Relay envelope + graceful secret rotation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;

&lt;span class="c1"&gt;# Two secrets — active and previous — to allow graceful rotation
&lt;/span&gt;&lt;span class="n"&gt;SECRETS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CURSOR_SECRET_V2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;  &lt;span class="c1"&gt;# current
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CURSOR_SECRET_V1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;# previous, may be None
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;ACTIVE_VERSION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;encode_cursor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;astimezone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;mid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dir&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;desc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ACTIVE_VERSION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlsafe_b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;separators&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;sort_keys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;SECRETS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ACTIVE_VERSION&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sha256&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlsafe_b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;decode_cursor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Peek at the version in the payload
&lt;/span&gt;    &lt;span class="n"&gt;padding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlsafe_b64decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SECRETS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cursor version &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; no longer supported&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlsafe_b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compare_digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cursor signature invalid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromisoformat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;messages_connection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channel_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;after&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;after&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;cts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decode_cursor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ApiError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;KEYSET_SQL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FIRST_PAGE_SQL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="n"&gt;has_next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;edges&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;node&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cursor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;encode_cursor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;created_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])}&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;edges&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;edges&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pageInfo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hasNextPage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;has_next&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hasPreviousPage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;after&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;endCursor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;edges&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cursor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;edges&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Payload includes &lt;code&gt;v: "v2"&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Version tag for secret rotation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Sign with &lt;code&gt;SECRETS[ACTIVE_VERSION]&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Current secret&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;On decode, peek &lt;code&gt;v&lt;/code&gt; before verification&lt;/td&gt;
&lt;td&gt;Pick which secret to verify against&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;If &lt;code&gt;v&lt;/code&gt; not in SECRETS → reject&lt;/td&gt;
&lt;td&gt;Cursor too old, client re-fetches page 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Verify signature constant-time&lt;/td&gt;
&lt;td&gt;Reject tampered cursors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;SQL uses keyset SQL if &lt;code&gt;after&lt;/code&gt;, initial otherwise&lt;/td&gt;
&lt;td&gt;O(log n + K) per page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Response uses Relay &lt;code&gt;edges + pageInfo&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Standard GraphQL envelope&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;LIMIT first + 1 for peek-ahead&lt;/td&gt;
&lt;td&gt;hasNextPage without extra query&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The design is production-ready: opaque signed cursors with version tags, graceful secret rotation via a two-secret map, Relay-standard response envelope, and the peek-ahead trick for hasNextPage. When a secret rotates, the new secret becomes v2 and the old becomes v1; clients with old cursors continue to work for one rotation window. After the window, old cursors return 400 and the client re-fetches page 1.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Wire cursor size&lt;/td&gt;
&lt;td&gt;~90 bytes (base64 payload + base64 signature)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verification time&lt;/td&gt;
&lt;td&gt;~50 μs (HMAC-SHA256 constant-time)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rotation window&lt;/td&gt;
&lt;td&gt;typically 24–72 hours (max client idle)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DB latency per page&lt;/td&gt;
&lt;td&gt;~5 ms on covering-indexed 10B-row table&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Opaque, version-tagged, HMAC-signed cursor&lt;/strong&gt;&lt;/strong&gt; — the payload includes &lt;code&gt;v: "v2"&lt;/code&gt; so the server knows which secret to verify against. Tamper-proof and forward-compatible with rotation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Constant-time signature compare&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;hmac.compare_digest&lt;/code&gt; prevents timing attacks. If you use &lt;code&gt;==&lt;/code&gt; on the signature, an attacker can measure the compare time and forge a valid cursor byte-by-byte.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Graceful rotation with a two-secret map&lt;/strong&gt;&lt;/strong&gt; — on rotation, promote v2 → v3 and v1 → v2 (drop the oldest). Clients with cursors signed by the previous secret continue to work until the window expires.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Relay envelope with peek-ahead&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;LIMIT first + 1&lt;/code&gt; fetches one extra row; &lt;code&gt;has_next = (fetched &amp;gt; first)&lt;/code&gt;; the extra row is discarded and its cursor becomes the next &lt;code&gt;after&lt;/code&gt;. No extra query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — HMAC verify O(1). Cursor decode O(payload size). SQL keyset O(log n + K) per page. Total per-request cost is dominated by the SQL — which is flat across pagination depth. The Relay envelope adds negligible overhead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — pagination&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Cursor pagination problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/pagination" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;


&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — window functions&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Window function drills&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/window-functions" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;





&lt;h2&gt;
  
  
  Cheat sheet — pagination recipe list
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two-primitive rule.&lt;/strong&gt; &lt;code&gt;sql pagination&lt;/code&gt; reduces to two primitives — OFFSET-based (page-numbered, admin dashboards, small tables) and keyset-based (infinite scroll, APIs, large mutable tables). Every real system ships one, the other, or both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OFFSET / LIMIT skeleton.&lt;/strong&gt; &lt;code&gt;SELECT ... ORDER BY sort_col, id LIMIT n OFFSET k&lt;/code&gt;. Cost &lt;code&gt;O(k + n)&lt;/code&gt; — fine up to &lt;code&gt;k ≈ 1,000&lt;/code&gt;, painful past &lt;code&gt;k ≥ 100,000&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ANSI OFFSET FETCH skeleton.&lt;/strong&gt; &lt;code&gt;SELECT ... ORDER BY sort_col, id OFFSET k ROWS FETCH FIRST n ROWS ONLY&lt;/code&gt;. Postgres, Oracle 12c+, SQL Server 2012+, Db2. Verbose but standard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL Server specifics.&lt;/strong&gt; &lt;code&gt;OFFSET k ROWS FETCH NEXT n ROWS ONLY&lt;/code&gt; — &lt;code&gt;NEXT&lt;/code&gt; is a synonym for &lt;code&gt;FIRST&lt;/code&gt;. &lt;strong&gt;ORDER BY is required&lt;/strong&gt; — the parser refuses without one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MySQL shorthand.&lt;/strong&gt; &lt;code&gt;LIMIT k, n&lt;/code&gt; — offset first, limit second (reversed operand order). Equivalent to &lt;code&gt;LIMIT n OFFSET k&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Oracle 11g legacy.&lt;/strong&gt; Nested &lt;code&gt;ROWNUM&lt;/code&gt; subquery — sort inside, number in the middle, filter outside. Migrate to &lt;code&gt;OFFSET ... FETCH FIRST&lt;/code&gt; on 12c+.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic ORDER BY rule.&lt;/strong&gt; Every paginated query needs a &lt;em&gt;unique&lt;/em&gt; tie-breaker (usually &lt;code&gt;id&lt;/code&gt;) — &lt;code&gt;ORDER BY sort_col DESC, id DESC&lt;/code&gt;. Ties without the tie-breaker drift across pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NULLS ordering rule.&lt;/strong&gt; Spell out &lt;code&gt;ORDER BY sort_col DESC NULLS LAST&lt;/code&gt;. Postgres/Oracle default to NULLs first; MySQL/SQL Server default to NULLs last; BigQuery to NULLs first. Never rely on the default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrent-write failure mode.&lt;/strong&gt; OFFSET is &lt;em&gt;positional&lt;/em&gt; and drifts under inserts / deletes — the classic "row seen twice" or "row skipped" bug. Keyset is &lt;em&gt;keyed&lt;/em&gt; and immune.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deep-OFFSET cost.&lt;/strong&gt; &lt;code&gt;O(k + n)&lt;/code&gt; — for &lt;code&gt;k = 1M&lt;/code&gt;, &lt;code&gt;n = 20&lt;/code&gt;, the engine walks 1M rows before emitting 20. Latency curve stays flat until it isn't, then climbs a cliff.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keyset skeleton.&lt;/strong&gt; &lt;code&gt;WHERE (sort_col, id) &amp;lt; (:cursor_col, :cursor_id) ORDER BY sort_col DESC, id DESC LIMIT n&lt;/code&gt;. Deep-page cost &lt;code&gt;O(log n + K)&lt;/code&gt; — flat forever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tuple comparison rule.&lt;/strong&gt; &lt;code&gt;(a, b) &amp;lt; (:a, :b)&lt;/code&gt; is &lt;em&gt;lexicographic&lt;/em&gt; — first &lt;code&gt;a &amp;lt; :a&lt;/code&gt;, or &lt;code&gt;a = :a AND b &amp;lt; :b&lt;/code&gt;. Not the same as &lt;code&gt;a &amp;lt; :a AND b &amp;lt; :b&lt;/code&gt; (which drops valid rows).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Composite index rule.&lt;/strong&gt; Keyset requires a composite index on the ORDER BY columns in the ORDER BY direction — &lt;code&gt;(sort_col DESC, id DESC)&lt;/code&gt;. Enables index-only backward scan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL Server row-value fallback.&lt;/strong&gt; Expand tuple compare to &lt;code&gt;sort_col &amp;lt; :cursor_ts OR (sort_col = :cursor_ts AND id &amp;lt; :cursor_id)&lt;/code&gt;. Same semantics, verbose but portable to older engines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mixed-direction ORDER BY.&lt;/strong&gt; For &lt;code&gt;ORDER BY score DESC, id ASC&lt;/code&gt;, expand to &lt;code&gt;score &amp;lt; :s OR (score = :s AND id &amp;gt; :i)&lt;/code&gt;. Row-value compare doesn't fit mixed directions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backward pagination.&lt;/strong&gt; Flip &lt;code&gt;&amp;lt;&lt;/code&gt; to &lt;code&gt;&amp;gt;&lt;/code&gt;, flip ORDER BY direction, reverse result in application code. Or use Relay's &lt;code&gt;last / before&lt;/code&gt; pair.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Peek-ahead trick.&lt;/strong&gt; &lt;code&gt;LIMIT n + 1&lt;/code&gt; — fetch one extra row, use its existence to compute &lt;code&gt;hasNextPage&lt;/code&gt;, discard from the response. Zero extra queries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Opaque cursor rule.&lt;/strong&gt; Cursors are base64-encoded JSON, HMAC-signed with a server-side secret. Clients treat them as black boxes. Never expose raw tuples.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cursor rotation rule.&lt;/strong&gt; Version-tag the payload; keep two active secrets (current + previous) for a rotation window equal to max client idle time. Reject cursors older than the window with a 400 and re-fetch-page-1 hint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relay Connections envelope.&lt;/strong&gt; &lt;code&gt;edges[]&lt;/code&gt; (with per-edge &lt;code&gt;cursor&lt;/code&gt;) + &lt;code&gt;pageInfo&lt;/code&gt; (&lt;code&gt;hasNextPage&lt;/code&gt;, &lt;code&gt;hasPreviousPage&lt;/code&gt;, &lt;code&gt;endCursor&lt;/code&gt;, &lt;code&gt;startCursor&lt;/code&gt;). GraphQL standard since 2015.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;REST cursor envelope.&lt;/strong&gt; &lt;code&gt;{"data": [...], "next_cursor": "eyJ...", "prev_cursor": null}&lt;/code&gt; — Stripe, GitHub, Twitter shapes. Or RFC-5988 &lt;code&gt;Link&lt;/code&gt; headers for HATEOAS-style clients.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Warehouse specifics.&lt;/strong&gt; Snowflake — cluster by sort_col; use tuple compare. BigQuery — partition by DATE(sort_col), cluster by sort_col, keyset with partition-prune predicate. Never use OFFSET past ~10K on either.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bounded-OFFSET safety net.&lt;/strong&gt; For legacy admin dashboards you can't migrate yet, cap &lt;code&gt;page × page_size ≤ 1M&lt;/code&gt;. Bounded worst-case latency of ~600 ms on a covering-indexed 100M-row table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SELECT COUNT(*)&lt;/code&gt; warning.&lt;/strong&gt; Total count on a mutable table is &lt;code&gt;O(n)&lt;/code&gt; regardless. On infinite scroll, don't return it. On admin dashboards, cache it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dialect matrix.&lt;/strong&gt; LIMIT / OFFSET: Postgres, MySQL, Snowflake, BigQuery, SQLite, Redshift, DuckDB. OFFSET / FETCH FIRST: Postgres, Oracle 12c+, SQL Server 2012+, Db2. Row-value compare: Postgres, MySQL 8, Oracle 12c+, Snowflake, BigQuery. Expanded form fallback: SQL Server, older MySQL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When OFFSET is fine.&lt;/strong&gt; Small tables (&amp;lt; 1M rows), aggregate dashboards, page-capped admin UIs (&lt;code&gt;page ≤ 5000&lt;/code&gt;), one-off batched exports. Everything else — keyset.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When keyset is fine.&lt;/strong&gt; Infinite-scroll feeds, mobile pagination, GraphQL Relay endpoints, big-table &lt;code&gt;list&lt;/code&gt; APIs, any endpoint expected to serve deep pages with stable results under concurrent writes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is SQL pagination and how do you choose between OFFSET and keyset?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;sql pagination&lt;/code&gt; is the primitive that lets a client fetch "the next N rows of an ordered result set" without re-fetching what came before. The two implementations you have to know are &lt;strong&gt;OFFSET-based&lt;/strong&gt; — &lt;code&gt;SELECT ... ORDER BY x LIMIT n OFFSET k&lt;/code&gt; — and &lt;strong&gt;keyset (seek method) based&lt;/strong&gt; — &lt;code&gt;WHERE (x, id) &amp;lt; (:cursor_x, :cursor_id) ORDER BY x DESC, id DESC LIMIT n&lt;/code&gt;. Choose OFFSET when the product requires random access to page N (admin dashboards, "jump to page 47" pagers), the table is small (&amp;lt; 1M rows) or aggregate-sized, and the page depth is capped (&lt;code&gt;page × page_size ≤ 1M&lt;/code&gt; on a covering-indexed table). Choose keyset for every consumer feed, every GraphQL Relay endpoint, every "list my orders / posts / messages" endpoint, and any pagination that must be &lt;strong&gt;stable under concurrent inserts and deletes&lt;/strong&gt; — because OFFSET is &lt;em&gt;positional&lt;/em&gt; and drifts under mutations, whereas keyset navigates by &lt;em&gt;key&lt;/em&gt; and is immune. The cost story cements the choice: OFFSET is &lt;code&gt;O(k + n)&lt;/code&gt; per page (linear in depth), keyset is &lt;code&gt;O(log n + K)&lt;/code&gt; per page (flat forever). On a 100M-row table with a covering index, OFFSET at page 100K takes about 1.4 s while keyset at any depth stays around 5 ms — a 300× gap that widens as the table grows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why is OFFSET slow on deep pages?
&lt;/h3&gt;

&lt;p&gt;Because the planner has no O(1) skip primitive on an ordered scan. When you write &lt;code&gt;LIMIT 20 OFFSET 1000000&lt;/code&gt;, the engine walks the ordered stream one row at a time, counting past a million rows, discarding each, before emitting the last twenty. Cost is &lt;code&gt;O(k + n)&lt;/code&gt; per page — for &lt;code&gt;k = 1M&lt;/code&gt;, that's a million row reads per page. On a covering-indexed table with &lt;code&gt;SELECT id, created_at&lt;/code&gt;, Postgres's &lt;code&gt;EXPLAIN (ANALYZE, BUFFERS)&lt;/code&gt; shows &lt;code&gt;Index Scan Backward using idx_events&lt;/code&gt; with &lt;code&gt;actual rows=20 actual loops=1000020&lt;/code&gt; — the "20 rows but a million loops" line is the smoking gun. Buffer pressure amplifies this — past a few hundred thousand rows, the working set exceeds &lt;code&gt;shared_buffers&lt;/code&gt; and reads spill from cache-hit to disk-read, latency jumps from milliseconds to seconds. Wide rows (with &lt;code&gt;SELECT *&lt;/code&gt; on a 1KB row) make it worse — each discarded row still pays its projection cost. And on columnar warehouses like BigQuery and Snowflake, per-column read overhead multiplies the effect further — deep OFFSET reads gigabytes of column data before returning bytes. Total cost across a full pagination from page 1 to page P is quadratic — &lt;code&gt;O(P² × n)&lt;/code&gt; — which is why every social feed on the internet moved off OFFSET a decade ago.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is keyset (seek method) pagination?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;keyset pagination&lt;/code&gt; (a.k.a. &lt;code&gt;seek method pagination&lt;/code&gt;) replaces "skip k rows" with "give me the rows strictly &lt;em&gt;after&lt;/em&gt; this cursor tuple." The two-line skeleton is &lt;code&gt;WHERE (sort_col, id) &amp;lt; (:cursor_col, :cursor_id) ORDER BY sort_col DESC, id DESC LIMIT n&lt;/code&gt; — the WHERE tuple comparison positions the scan at the cursor via an index seek on the composite &lt;code&gt;(sort_col DESC, id DESC)&lt;/code&gt; index, and the LIMIT bounds the forward scan to exactly n rows. Total cost is &lt;code&gt;O(log n + K)&lt;/code&gt; per page — the index seek is O(log n) (depth of the btree), and K = LIMIT sequential reads finish it. Every page — page 1 or page 1,000,000 — hits the exact same plan shape, so latency stays flat across the entire pagination depth. The tuple comparison &lt;code&gt;(a, b) &amp;lt; (:a, :b)&lt;/code&gt; is &lt;em&gt;lexicographic&lt;/em&gt; — first &lt;code&gt;a &amp;lt; :a&lt;/code&gt;, or &lt;code&gt;a = :a AND b &amp;lt; :b&lt;/code&gt; — supported natively on Postgres, MySQL 8, Oracle 12c+, Snowflake, and BigQuery. On SQL Server and older MySQL, expand to &lt;code&gt;a &amp;lt; :a OR (a = :a AND b &amp;lt; :b)&lt;/code&gt; — same semantics, verbose but portable. The trade-off is no random access — you can't ask for "page 47" — but every infinite-scroll consumer feed on the internet uses keyset because it beats OFFSET on both cost and consistency.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you build cursor pagination for a REST or GraphQL API?
&lt;/h3&gt;

&lt;p&gt;Cursors are &lt;strong&gt;opaque base64-encoded, HMAC-signed tokens&lt;/strong&gt; — clients treat them as black boxes and pass them back verbatim on the next call. The payload is a JSON blob &lt;code&gt;{"ts": "2026-07-05T12:00:00Z", "id": 5000000, "dir": "desc", "v": "v2"}&lt;/code&gt; — base64url-encoded, then HMAC-SHA256-signed with a server-side secret, then joined as &lt;code&gt;payload_b64.signature_b64&lt;/code&gt; (JWT-like without a header). Servers verify the signature in constant time (&lt;code&gt;hmac.compare_digest&lt;/code&gt;) before decoding, so tampered cursors are rejected before any SQL runs. For GraphQL, follow the Relay Connections spec — &lt;code&gt;orders(first: 20, after: "eyJ...")&lt;/code&gt; returns &lt;code&gt;edges[]&lt;/code&gt; (with a per-edge &lt;code&gt;cursor&lt;/code&gt;) plus &lt;code&gt;pageInfo&lt;/code&gt; (&lt;code&gt;hasNextPage&lt;/code&gt;, &lt;code&gt;endCursor&lt;/code&gt;, &lt;code&gt;startCursor&lt;/code&gt;). For REST, use &lt;code&gt;{"data": [...], "next_cursor": "eyJ...", "prev_cursor": null}&lt;/code&gt; (Stripe / GitHub style) or RFC-5988 &lt;code&gt;Link&lt;/code&gt; headers (&lt;code&gt;&amp;lt;https://.../orders?cursor=eyJ...&amp;gt;; rel="next"&lt;/code&gt;). The &lt;code&gt;hasNextPage&lt;/code&gt; uses the peek-ahead trick — fetch &lt;code&gt;LIMIT n + 1&lt;/code&gt;, use the existence of the (n+1)th row as &lt;code&gt;hasNextPage&lt;/code&gt;, discard it before returning. Rotate the signing secret every 90 days with a two-secret map (current + previous) so cursors survive one rotation window; reject older versions with a 400 and a hint to re-fetch page 1.&lt;/p&gt;

&lt;h3&gt;
  
  
  How does pagination differ across Postgres, MySQL, SQL Server, Oracle, Snowflake, and BigQuery?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Postgres&lt;/strong&gt; — &lt;code&gt;LIMIT n OFFSET k&lt;/code&gt; (native) and &lt;code&gt;OFFSET k ROWS FETCH FIRST n ROWS ONLY&lt;/code&gt; (ANSI); full row-value comparison &lt;code&gt;(a, b) &amp;lt; (:a, :b)&lt;/code&gt; for keyset; composite descending indexes since forever. Best case. &lt;strong&gt;MySQL 8&lt;/strong&gt; — &lt;code&gt;LIMIT n OFFSET k&lt;/code&gt; or shorthand &lt;code&gt;LIMIT k, n&lt;/code&gt; (reversed operands); row-value compare since 8.0.16; descending indexes since 8.0.13. Full keyset support. &lt;strong&gt;SQL Server 2012+&lt;/strong&gt; — &lt;code&gt;OFFSET k ROWS FETCH NEXT n ROWS ONLY&lt;/code&gt; with &lt;strong&gt;required ORDER BY&lt;/strong&gt;; no row-value compare (use expanded &lt;code&gt;a &amp;lt; :a OR (a = :a AND b &amp;lt; :b)&lt;/code&gt; form); no &lt;code&gt;NULLS FIRST/LAST&lt;/code&gt; (use &lt;code&gt;CASE WHEN … IS NULL&lt;/code&gt; workaround). &lt;strong&gt;Oracle 12c+&lt;/strong&gt; — &lt;code&gt;OFFSET k ROWS FETCH FIRST n ROWS ONLY&lt;/code&gt; (ANSI); full row-value compare; pre-12c uses nested &lt;code&gt;ROWNUM&lt;/code&gt; subquery. &lt;strong&gt;Snowflake&lt;/strong&gt; — &lt;code&gt;LIMIT n OFFSET k&lt;/code&gt;; row-value compare supported; no btree indexes — use &lt;code&gt;CLUSTER BY (sort_col)&lt;/code&gt; for micro-partition co-location; avoid OFFSET past ~10K. &lt;strong&gt;BigQuery&lt;/strong&gt; — &lt;code&gt;LIMIT n OFFSET k&lt;/code&gt;; row-value compare since 2019; no btree — use &lt;code&gt;PARTITION BY DATE(sort_col) CLUSTER BY sort_col&lt;/code&gt; and combine keyset with an outer partition-prune predicate; OFFSET is documented as an anti-pattern past a few thousand rows. Learn the six spellings; every migration between engines rewrites pagination as a mechanical translation.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you handle ties and NULLs in a paginated ORDER BY?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Ties&lt;/strong&gt; — two rows sharing the ORDER BY key (same &lt;code&gt;created_at&lt;/code&gt; to the millisecond, same score in a leaderboard) can alternate between pages unless you add a &lt;strong&gt;unique tie-breaker&lt;/strong&gt;. Every paginated ORDER BY ends with a unique column, usually the primary key: &lt;code&gt;ORDER BY created_at DESC, id DESC&lt;/code&gt;. The tuple comparison in keyset must include the tie-breaker too — &lt;code&gt;(created_at, id) &amp;lt; (:cursor_ts, :cursor_id)&lt;/code&gt;. Without the tie-breaker, &lt;code&gt;WHERE created_at &amp;lt; :ts&lt;/code&gt; would skip &lt;em&gt;all&lt;/em&gt; rows tied on &lt;code&gt;:ts&lt;/code&gt; even if only one was the cursor row. &lt;strong&gt;NULLs&lt;/strong&gt; — Postgres and Oracle default to NULLs first for DESC; MySQL and SQL Server default to NULLs last; BigQuery defaults to NULLs first. Never rely on the default — spell it out: &lt;code&gt;ORDER BY created_at DESC NULLS LAST&lt;/code&gt;. Postgres, Oracle, MySQL 8, and BigQuery all support the &lt;code&gt;NULLS FIRST / LAST&lt;/code&gt; syntax; SQL Server doesn't — use the &lt;code&gt;CASE WHEN created_at IS NULL THEN 1 ELSE 0 END, created_at DESC&lt;/code&gt; workaround. For keyset with a nullable sort column, either mark the column NOT NULL (the safest fix) or use &lt;code&gt;COALESCE(created_at, '-infinity')&lt;/code&gt; on both sides of the comparison to give NULLs a defined position. Every keyset paginator ships either the NOT NULL constraint or the coalescing predicate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practice on PipeCode
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Drill the &lt;a href="https://pipecode.ai/explore/practice/topic/pagination" rel="noopener noreferrer"&gt;SQL pagination practice library →&lt;/a&gt; for &lt;code&gt;LIMIT / OFFSET&lt;/code&gt;, &lt;code&gt;FETCH FIRST n ROWS ONLY&lt;/code&gt;, keyset tuple comparison, and cursor endpoints across Postgres, MySQL 8, SQL Server, Oracle, Snowflake, and BigQuery dialects.&lt;/li&gt;
&lt;li&gt;Rehearse on &lt;a href="https://pipecode.ai/explore/practice/topic/order-by-limit" rel="noopener noreferrer"&gt;ORDER BY + LIMIT problems →&lt;/a&gt; — the ordering, tie-breaker, and NULL discipline that make every paginated query deterministic.&lt;/li&gt;
&lt;li&gt;Sharpen the &lt;a href="https://pipecode.ai/explore/practice/topic/indexing" rel="noopener noreferrer"&gt;SQL indexing drill room →&lt;/a&gt; for composite &lt;code&gt;(sort_col, id)&lt;/code&gt; indexes, index-only backward scans, and the difference between OFFSET's &lt;code&gt;O(k)&lt;/code&gt; and keyset's &lt;code&gt;O(log n + K)&lt;/code&gt; deep-page paths.&lt;/li&gt;
&lt;li&gt;Push the difficulty ceiling with &lt;a href="https://pipecode.ai/explore/practice/topic/optimization" rel="noopener noreferrer"&gt;SQL optimization drills →&lt;/a&gt; — read &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; plans, spot the OFFSET trap, and migrate legacy paginators to keyset.&lt;/li&gt;
&lt;li&gt;Warm up with &lt;a href="https://pipecode.ai/explore/practice/topic/top-n" rel="noopener noreferrer"&gt;top-N per group problems →&lt;/a&gt; — the sibling primitive to pagination, sharing the composite &lt;code&gt;(partition_col, sort_col, id)&lt;/code&gt; index pattern.&lt;/li&gt;
&lt;li&gt;Layer &lt;a href="https://pipecode.ai/explore/practice/topic/window-functions" rel="noopener noreferrer"&gt;window function drills →&lt;/a&gt; — the &lt;code&gt;ROW_NUMBER() / RANK() / DENSE_RANK()&lt;/code&gt; family that shows up when a legacy paginator can't be migrated to keyset yet.&lt;/li&gt;
&lt;li&gt;Sharpen the general SQL surface with the &lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;SQL practice library →&lt;/a&gt; which contains 450+ DE-focused questions covering pagination, keyset cursors, index alignment, and every adjacent pattern.&lt;/li&gt;
&lt;li&gt;For the broader SQL interview surface, take the &lt;a href="https://pipecode.ai/explore/courses/sql-for-data-engineering-interviews-from-zero-to-faang" rel="noopener noreferrer"&gt;SQL for Data Engineering course →&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/" rel="noopener noreferrer"&gt;Pipecode.ai&lt;/a&gt; is Leetcode for Data Engineering — every `sql pagination` recipe above ships with hands-on practice rooms where you type the OFFSET / LIMIT skeleton, migrate it to `sql offset fetch` (ANSI) and `OFFSET ... FETCH NEXT` (SQL Server) side by side, benchmark the `sql limit offset performance` cliff at page 1M with `EXPLAIN (ANALYZE, BUFFERS)`, rewrite the same paginator as `keyset pagination` with tuple comparison, align the composite index for `seek method pagination`'s `O(log n)` deep-page path, wire the whole thing behind an opaque HMAC-signed cursor for `sql cursor pagination`, and finally serve the Relay `edges / pageInfo` envelope that every `sql infinite scroll` feed on the internet ships. PipeCode pairs every reading with 450+ DE-focused problems and a real-time scoring engine, so you never have to wonder whether your pagination answer holds up under a senior interviewer's depth probes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/pagination" rel="noopener noreferrer"&gt;Practice pagination now →&lt;/a&gt;&lt;br&gt;
&lt;a href="https://pipecode.ai/explore/practice/topic/indexing" rel="noopener noreferrer"&gt;Indexing drills →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>sql</category>
      <category>interview</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>SQL LATERAL Joins &amp; CROSS APPLY: When They Beat Subqueries</title>
      <dc:creator>Gowtham Potureddi</dc:creator>
      <pubDate>Sat, 11 Jul 2026 15:31:46 +0000</pubDate>
      <link>https://dev.to/gowthampotureddi/sql-lateral-joins-cross-apply-when-they-beat-subqueries-45g1</link>
      <guid>https://dev.to/gowthampotureddi/sql-lateral-joins-cross-apply-when-they-beat-subqueries-45g1</guid>
      <description>&lt;p&gt;&lt;strong&gt;&lt;code&gt;sql lateral join&lt;/code&gt;&lt;/strong&gt; is the single most under-taught primitive in modern SQL — and the single largest tool a senior data engineer can pull out of the ANSI SQL/1999 standard when the plain correlated subquery hits a wall. Once you internalise "LATERAL means: for each outer row, run this inner query with access to the outer columns," a whole family of otherwise-painful queries collapses into a two-line skeleton — the classic &lt;code&gt;sql top-n per group&lt;/code&gt; ("give me the last three orders per customer"), JSON and array unnest with correlation, table-valued function composition, and every other "run a mini-query per row" ask an interviewer might throw at a whiteboard.&lt;/p&gt;

&lt;p&gt;This guide is the mid-to-senior tour you wished existed the first time a reviewer asked why your query was doing an N × subquery scan, when an interviewer probed the difference between &lt;code&gt;lateral vs subquery&lt;/code&gt; semantics, when the team lead demanded a &lt;code&gt;cross apply sql server&lt;/code&gt; port of your &lt;code&gt;postgres lateral&lt;/code&gt; recipe, when the analyst asked whether &lt;code&gt;bigquery lateral&lt;/code&gt; was a thing, or when the on-call engineer showed a &lt;code&gt;outer apply&lt;/code&gt; plan running 10× faster than the LEFT JOIN + &lt;code&gt;ROW_NUMBER()&lt;/code&gt; alternative. It walks through the LATERAL syntax and mental model (LEFT JOIN LATERAL vs CROSS JOIN LATERAL, correlation binding, set-returning-function unnest), the CROSS APPLY / OUTER APPLY family that SQL Server ships as its LATERAL equivalent (the translation table, APPLY + TVF composition, JSON_TABLE / OPENJSON unnest), the top-N-per-group performance story (LATERAL + LIMIT with an index seek versus &lt;code&gt;ROW_NUMBER() OVER&lt;/code&gt; with a sort), and the six-engine performance matrix (Postgres, MySQL 8, Oracle 12c+, SQL Server, Snowflake, BigQuery) so you always know which keyword to reach for on which warehouse. Every section pairs a teaching block with a Solution-Tail interview answer — code, a step-by-step trace, an output table, then a concept-by-concept breakdown of why it works.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxu3zwqafupemc3v15lu9.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxu3zwqafupemc3v15lu9.jpeg" alt="PipeCode blog header for SQL LATERAL joins and CROSS APPLY — bold white headline 'SQL LATERAL &amp;amp; CROSS APPLY' with subtitle 'For each row, run this query' and a stylised per-row correlated-arrow scene between an outer table and an inner query card on a dark gradient with purple, green, orange, and blue accents and a small pipecode.ai attribution." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you want &lt;strong&gt;hands-on reps&lt;/strong&gt; immediately after reading, drill the &lt;a href="https://pipecode.ai/explore/practice/topic/joins/sql" rel="noopener noreferrer"&gt;SQL joins practice library →&lt;/a&gt;, rehearse on &lt;a href="https://pipecode.ai/explore/practice/topic/top-n" rel="noopener noreferrer"&gt;top-N-per-group problems →&lt;/a&gt;, and sharpen the correlated-subquery axis with the &lt;a href="https://pipecode.ai/explore/practice/topic/subqueries" rel="noopener noreferrer"&gt;subqueries drill room →&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;On this page&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why LATERAL matters in 2026&lt;/li&gt;
&lt;li&gt;LATERAL syntax &amp;amp; mental model&lt;/li&gt;
&lt;li&gt;CROSS APPLY &amp;amp; OUTER APPLY&lt;/li&gt;
&lt;li&gt;Top-N per group pattern&lt;/li&gt;
&lt;li&gt;When LATERAL beats subquery — perf matrix&lt;/li&gt;
&lt;li&gt;Cheat sheet — LATERAL / CROSS APPLY recipe list&lt;/li&gt;
&lt;li&gt;Frequently asked questions&lt;/li&gt;
&lt;li&gt;Practice on PipeCode&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1. Why LATERAL matters in 2026
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The "for each outer row, run this inner query" primitive — the missing piece between joins, subqueries, and window functions
&lt;/h3&gt;

&lt;p&gt;The one-sentence invariant: &lt;strong&gt;a &lt;code&gt;sql lateral join&lt;/code&gt; runs the right-hand subquery once per row of the left-hand relation, with the inner query allowed to reference the outer row's columns — turning "SELECT the top-3 orders per customer" from a window-function-plus-filter puzzle into a two-line correlated join&lt;/strong&gt;. Once you internalise "for each outer row, run this query," the LATERAL interview surface collapses into a single mental model with a handful of dialect variants.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where LATERAL fits in the join family.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Inner join / outer join.&lt;/strong&gt; Rows are matched on an equality (or range) predicate. Both sides are evaluated independently, then the planner picks a join algorithm — hash, merge, or nested loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correlated subquery in the SELECT list.&lt;/strong&gt; For each outer row, the planner runs the scalar subquery to produce one column value. Cost is &lt;code&gt;N × cost(subquery)&lt;/code&gt; in the worst case — many planners fail to unnest correlated scalars.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correlated subquery in the WHERE / EXISTS clause.&lt;/strong&gt; For each outer row, the planner runs the boolean subquery to filter. Modern planners try hard to rewrite these to a semi-join, but a rewrite is not guaranteed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LATERAL / CROSS APPLY.&lt;/strong&gt; For each outer row, the planner runs a &lt;em&gt;table-producing&lt;/em&gt; subquery — and joins the outer row to &lt;em&gt;each&lt;/em&gt; of the inner rows returned. The inner query can reference outer columns freely. Semantically, LATERAL is a correlated join that produces zero, one, or many inner rows per outer row.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why the primitive is under-taught.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The SQL/1999 standard introduced the keyword, but implementations lagged for over a decade. Postgres added it in 9.3 (2013), MySQL waited until 8.0.14 (2019), Oracle shipped it in 12c (2013), and SQL Server never adopted the keyword — instead offering &lt;code&gt;CROSS APPLY&lt;/code&gt; and &lt;code&gt;OUTER APPLY&lt;/code&gt; since 2005 with identical semantics.&lt;/li&gt;
&lt;li&gt;Every classic SQL textbook was written before LATERAL was widely available, so an entire generation of engineers learned to reach for &lt;code&gt;WHERE (partition, ranked_col) IN (SELECT ...)&lt;/code&gt; or window-function-plus-filter patterns even when a two-line LATERAL would beat both.&lt;/li&gt;
&lt;li&gt;LATERAL is also invisible to the naive query author — you have to know the keyword to type it. There is no CTE-style rewrite that emerges from re-reading your own draft.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Dialect coverage in 2026 — the six engines you actually ship to.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Postgres 9.3+.&lt;/strong&gt; Full support. Reads exactly like the ANSI syntax — &lt;code&gt;SELECT ... FROM outer, LATERAL (SELECT ... WHERE inner.k = outer.k) AS inner&lt;/code&gt;. Both &lt;code&gt;CROSS JOIN LATERAL&lt;/code&gt; and &lt;code&gt;LEFT JOIN LATERAL ... ON TRUE&lt;/code&gt; variants supported.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MySQL 8.0.14+.&lt;/strong&gt; Full support. Same syntax as Postgres. Older MySQL versions (5.7, 8.0.0–8.0.13) require the &lt;code&gt;ROW_NUMBER()&lt;/code&gt; workaround.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Oracle 12c+.&lt;/strong&gt; Ships both &lt;code&gt;LATERAL&lt;/code&gt; (ANSI) and &lt;code&gt;CROSS APPLY&lt;/code&gt; / &lt;code&gt;OUTER APPLY&lt;/code&gt; (SQL-Server-compatible). Interchangeable — pick the one your team reads more fluently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL Server 2005+.&lt;/strong&gt; No &lt;code&gt;LATERAL&lt;/code&gt; keyword. Uses &lt;code&gt;CROSS APPLY&lt;/code&gt; (inner) and &lt;code&gt;OUTER APPLY&lt;/code&gt; (left) instead. Same semantics as ANSI LATERAL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snowflake.&lt;/strong&gt; Full &lt;code&gt;LATERAL&lt;/code&gt; support since 2020. Also supports the &lt;code&gt;FLATTEN&lt;/code&gt; table function inside LATERAL for JSON / array unnest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BigQuery.&lt;/strong&gt; No explicit &lt;code&gt;LATERAL&lt;/code&gt; keyword. Correlation is implicit — an unnested array reference in the &lt;code&gt;FROM&lt;/code&gt; clause behaves like &lt;code&gt;CROSS JOIN LATERAL UNNEST(...)&lt;/code&gt;. Scalar correlation is expressed via subqueries, not LATERAL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Three problem shapes LATERAL nails.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Top-N per group.&lt;/strong&gt; "Latest 3 orders per customer" — the archetype. LATERAL + &lt;code&gt;LIMIT 3&lt;/code&gt; compiles to a nested-loop-with-limit plan that stops fetching inner rows the moment the third one arrives; &lt;code&gt;ROW_NUMBER() OVER + WHERE rn ≤ 3&lt;/code&gt; has to materialise every ranked row across the whole table before filtering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON / array unnest with correlation.&lt;/strong&gt; "Unroll every item in the JSON payload, keeping the parent row." LATERAL + &lt;code&gt;jsonb_array_elements(payload)&lt;/code&gt; in Postgres, &lt;code&gt;CROSS APPLY OPENJSON(payload)&lt;/code&gt; in SQL Server. The unnest function must see the outer row's payload column — that correlation is exactly what LATERAL exists for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TVF composition.&lt;/strong&gt; "Run this parametrised table-valued function per row of an input list." &lt;code&gt;SELECT c.*, t.* FROM customers c CROSS APPLY dbo.top_orders(c.id) t&lt;/code&gt; on SQL Server; the same pattern with a Postgres SETOF-returning function inside &lt;code&gt;LATERAL&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why interviewers love LATERAL as a probe.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's a compact fluency check.&lt;/strong&gt; A senior candidate reaches for LATERAL on the top-3-orders-per-customer question in the first minute; a junior reaches for &lt;code&gt;ROW_NUMBER() OVER&lt;/code&gt; and has to be nudged toward the LATERAL rewrite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It forces mental-model precision.&lt;/strong&gt; "What does the inner query see?" (the current outer row's columns) "What does the outer query see?" (each inner row emitted). "What if the inner returns zero rows?" (CROSS JOIN LATERAL drops the outer row; LEFT JOIN LATERAL keeps it with NULLs).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It reveals planner intuition.&lt;/strong&gt; "When does LATERAL beat a plain correlated subquery?" (when the planner can push a limit or unique into the inner scan). "When does it lose?" (when N is huge and the inner query can't be indexed).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It exposes dialect awareness.&lt;/strong&gt; "Would this run on SQL Server?" — a candidate who instantly answers "yes, but you write CROSS APPLY instead of CROSS JOIN LATERAL" signals cross-warehouse fluency.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Where LATERAL / APPLY lands in your pipelines.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Analytics on wide fact tables.&lt;/strong&gt; "Attach the most recent status change to each order," "attach the top three sessions per user," "attach the last five sensor readings to each device row." All classic top-N-per-group patterns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feature engineering.&lt;/strong&gt; "For each user row, compute the sum of the last 30 days of spend." LATERAL + windowed aggregate inside — the inner query filters by the outer's &lt;code&gt;user_id&lt;/code&gt; and produces one summary row per outer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ETL of nested data.&lt;/strong&gt; "Explode each JSON payload into its item rows, keeping the parent metadata." LATERAL + &lt;code&gt;jsonb_array_elements&lt;/code&gt; / &lt;code&gt;OPENJSON&lt;/code&gt; / &lt;code&gt;UNNEST&lt;/code&gt; — one row per (parent, item) pair.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stored-procedure-to-SQL migrations.&lt;/strong&gt; Legacy code with a WHILE loop that runs a query per row of a driver table maps directly to LATERAL / CROSS APPLY over that driver table.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What senior interviewers actually probe.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Do you know the syntax?&lt;/strong&gt; &lt;code&gt;CROSS JOIN LATERAL (…)&lt;/code&gt; (inner) vs &lt;code&gt;LEFT JOIN LATERAL (…) ON TRUE&lt;/code&gt; (left). The &lt;code&gt;ON TRUE&lt;/code&gt; is a common typo target — every candidate forgets it once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do you know the semantic difference?&lt;/strong&gt; Inner drops outer rows with no inner match; left keeps them with NULLs. Same as regular join families.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do you know the SQL Server keyword?&lt;/strong&gt; &lt;code&gt;CROSS APPLY&lt;/code&gt; = &lt;code&gt;CROSS JOIN LATERAL&lt;/code&gt;; &lt;code&gt;OUTER APPLY&lt;/code&gt; = &lt;code&gt;LEFT JOIN LATERAL ... ON TRUE&lt;/code&gt;. Interchangeable on Oracle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do you know when LATERAL wins on perf?&lt;/strong&gt; When the inner query is expensive per row and can be indexed with &lt;code&gt;(partition_col, order_col)&lt;/code&gt; — the nested-loop-with-limit plan is dramatically cheaper than a sort-and-rank plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do you know the BigQuery variant?&lt;/strong&gt; No LATERAL keyword; &lt;code&gt;UNNEST&lt;/code&gt; in the &lt;code&gt;FROM&lt;/code&gt; clause is implicitly correlated. For scalar-correlation asks, BigQuery falls back to subqueries.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Worked example — top-3 orders per customer, the first LATERAL query most engineers write
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The archetype: given &lt;code&gt;customers(id, name)&lt;/code&gt; and &lt;code&gt;orders(id, customer_id, order_ts, total)&lt;/code&gt;, return the three most recent orders per customer. This is the fastest way to spot whether a candidate has ever reached for LATERAL — the two-line answer is a fingerprint on the whiteboard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write a Postgres query that returns, for each customer, the three most recent orders (order_ts, total). Use LATERAL. Assume there is an index on &lt;code&gt;orders(customer_id, order_ts DESC)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;customer_id&lt;/th&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Alice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Bob&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Carol&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;customer_id&lt;/th&gt;
&lt;th&gt;order_ts&lt;/th&gt;
&lt;th&gt;total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2026-07-01&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;102&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2026-07-05&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;103&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2026-07-08&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;104&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2026-07-09&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;105&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2026-07-02&lt;/td&gt;
&lt;td&gt;70&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;106&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2026-07-07&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="k"&gt;LATERAL&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
  &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;FROM customers c&lt;/code&gt; — the outer relation. Every row here is visited once.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CROSS JOIN LATERAL (…) o&lt;/code&gt; — for each outer row &lt;code&gt;c&lt;/code&gt;, run the parenthesised subquery. The subquery reads &lt;code&gt;c.id&lt;/code&gt; freely — that is the correlation binding.&lt;/li&gt;
&lt;li&gt;Inside the LATERAL — &lt;code&gt;WHERE o.customer_id = c.id ORDER BY o.order_ts DESC LIMIT 3&lt;/code&gt;. With the index on &lt;code&gt;(customer_id, order_ts DESC)&lt;/code&gt;, this is a single index seek plus a bounded scan of at most three rows.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CROSS JOIN LATERAL&lt;/code&gt; drops any customer with no matching orders. To keep customers with no orders, swap to &lt;code&gt;LEFT JOIN LATERAL (…) ON TRUE&lt;/code&gt; and the missing-inner rows become NULL.&lt;/li&gt;
&lt;li&gt;Final &lt;code&gt;ORDER BY c.id, o.order_ts DESC&lt;/code&gt; produces a stable output — customers grouped, each customer's orders newest first.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;customer_id&lt;/th&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;order_ts&lt;/th&gt;
&lt;th&gt;total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Alice&lt;/td&gt;
&lt;td&gt;2026-07-09&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Alice&lt;/td&gt;
&lt;td&gt;2026-07-08&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Alice&lt;/td&gt;
&lt;td&gt;2026-07-05&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Bob&lt;/td&gt;
&lt;td&gt;2026-07-07&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Bob&lt;/td&gt;
&lt;td&gt;2026-07-02&lt;/td&gt;
&lt;td&gt;70&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; LATERAL + &lt;code&gt;LIMIT K&lt;/code&gt; with a covering index is the fastest top-N-per-group plan on Postgres, MySQL 8, and Oracle. The nested-loop-with-limit stops early per customer, so total cost is &lt;code&gt;K × outer_rows × log(inner_rows)&lt;/code&gt; — dramatically better than the &lt;code&gt;ROW_NUMBER() OVER + WHERE rn ≤ 3&lt;/code&gt; sort-then-filter plan on skewed data.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — LATERAL vs plain correlated subquery — where the semantics diverge
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A common source of confusion — the difference between "correlated subquery in the SELECT list" (returns exactly one value) and "LATERAL subquery in the FROM clause" (returns zero-to-many rows). The interviewer wants to hear you say "correlated scalar subqueries are one-column-one-row; LATERAL is one-outer-row-to-many-inner-rows."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given the same tables, write two versions of "for each customer, get the most recent order total": one using a correlated scalar subquery, one using LATERAL. Explain the semantic difference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt; (Same as above.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Version A: correlated scalar subquery in SELECT list&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;
   &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
   &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
   &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
   &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;last_total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Version B: LATERAL join&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;last_total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;last_order_ts&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="k"&gt;LATERAL&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
  &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Version A returns exactly one column (&lt;code&gt;last_total&lt;/code&gt;) per customer. If the inner query returned two columns, the whole query would fail — the SELECT-list subquery must be scalar (one column, one row).&lt;/li&gt;
&lt;li&gt;Version B returns &lt;em&gt;many&lt;/em&gt; columns from the inner query (&lt;code&gt;total&lt;/code&gt;, &lt;code&gt;order_ts&lt;/code&gt;, and any others you project). The LATERAL join yields a full inner row per outer, projected alongside the outer columns.&lt;/li&gt;
&lt;li&gt;Version A silently returns NULL for customers with no orders — the subquery returns zero rows, which the SELECT-list treats as NULL.&lt;/li&gt;
&lt;li&gt;Version B with &lt;code&gt;LEFT JOIN LATERAL ... ON TRUE&lt;/code&gt; behaves the same way — outer row preserved with NULL inner columns. With &lt;code&gt;CROSS JOIN LATERAL&lt;/code&gt;, customers with no orders would be dropped entirely.&lt;/li&gt;
&lt;li&gt;Perf-wise, most planners rewrite Version A to Version B under the hood — but "most" is not "all." Older planners and specific dialects (BigQuery on certain shapes) leave the correlated scalar as an N × subquery plan.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output&lt;/strong&gt; (both versions).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;customer_id&lt;/th&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;last_total&lt;/th&gt;
&lt;th&gt;last_order_ts&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Alice&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;2026-07-09&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Bob&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;2026-07-07&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Carol&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Reach for LATERAL the moment you need more than one column from the inner query per outer row, or when you want explicit control over the plan. Correlated scalar subqueries are fine for one-column-one-row lookups, but only when you trust the planner to unnest them.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — engine dialect quickstart
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A quick sanity check the interviewer might drop: "which of these engines can I run LATERAL / APPLY on today?" The answer is a short six-row list — knowing it cold is a senior signal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given a list of engines, mark each with the LATERAL / APPLY keyword it supports as of 2026.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt; Postgres 16, MySQL 8.0.14+, Oracle 12c+, SQL Server 2022, Snowflake, BigQuery.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt; (No code — a dialect matrix answer.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Engine          | LATERAL support (as of 2026)              | Notes
Postgres 9.3+   | LATERAL (ANSI), CROSS/LEFT JOIN LATERAL   | first mover in OSS
MySQL 8.0.14+   | LATERAL (ANSI), CROSS/LEFT JOIN LATERAL   | 5.7 lacks it
Oracle 12c+     | LATERAL + CROSS APPLY + OUTER APPLY        | ships both keywords
SQL Server 2005+| CROSS APPLY + OUTER APPLY (no LATERAL kw)  | same semantics
Snowflake       | LATERAL, LATERAL FLATTEN                    | FLATTEN for JSON
BigQuery        | UNNEST is implicit LATERAL; no keyword     | correlated array only
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Postgres was the OSS first-mover in 2013. If you're on Postgres 12+, LATERAL is table stakes.&lt;/li&gt;
&lt;li&gt;MySQL waited until 8.0.14 in 2019. If your project supports MySQL 5.7, LATERAL is off-limits; use the &lt;code&gt;ROW_NUMBER()&lt;/code&gt; workaround.&lt;/li&gt;
&lt;li&gt;Oracle 12c (2013) ships both keywords — LATERAL (ANSI) and CROSS/OUTER APPLY (SQL-Server-compatible). Pick whichever your team reads better.&lt;/li&gt;
&lt;li&gt;SQL Server never added LATERAL. Every LATERAL query maps mechanically to CROSS APPLY (inner) or OUTER APPLY (left).&lt;/li&gt;
&lt;li&gt;Snowflake ships LATERAL and adds LATERAL FLATTEN for VARIANT / JSON columns — the standard unnest pattern for semi-structured data.&lt;/li&gt;
&lt;li&gt;BigQuery has no LATERAL keyword. Array unnest via &lt;code&gt;UNNEST(...)&lt;/code&gt; in the FROM clause is implicitly correlated; scalar correlations use subqueries.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt; The matrix above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Memorise the six-row list. When an interviewer asks "would you use LATERAL here?" the first question back is "what's the warehouse?" — say it out loud and you signal senior instincts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on the LATERAL mental model
&lt;/h3&gt;

&lt;p&gt;A senior interviewer often opens with: "I hand you two tables — customers and orders — and ask for the two most-recent orders per customer. Walk me through the LATERAL query, tell me what changes on SQL Server, and tell me when this beats &lt;code&gt;ROW_NUMBER() OVER + WHERE rn ≤ 2&lt;/code&gt;."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using CROSS JOIN LATERAL with LIMIT 2
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ANSI / Postgres / MySQL 8 / Oracle 12c+ / Snowflake&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="k"&gt;LATERAL&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
  &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- SQL Server 2005+ — same semantics, CROSS APPLY keyword&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="n"&gt;APPLY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;TOP&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Outer row (customer_id)&lt;/th&gt;
&lt;th&gt;Inner query result&lt;/th&gt;
&lt;th&gt;Emitted rows&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;c1 = Alice&lt;/td&gt;
&lt;td&gt;last 2 = (07-09, 60), (07-08, 90)&lt;/td&gt;
&lt;td&gt;(Alice, 07-09, 60), (Alice, 07-08, 90)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;c2 = Bob&lt;/td&gt;
&lt;td&gt;last 2 = (07-07, 30), (07-02, 70)&lt;/td&gt;
&lt;td&gt;(Bob, 07-07, 30), (Bob, 07-02, 70)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;c3 = Carol&lt;/td&gt;
&lt;td&gt;last 2 = (empty)&lt;/td&gt;
&lt;td&gt;(dropped — CROSS JOIN LATERAL)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Sort&lt;/td&gt;
&lt;td&gt;ORDER BY c.id, o.order_ts DESC&lt;/td&gt;
&lt;td&gt;Final output&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The engine walks each customer, runs the inner LIMIT 2 query with the outer's &lt;code&gt;c.id&lt;/code&gt; bound in, and emits at most two rows per customer. Carol has no orders — the CROSS JOIN LATERAL drops her; swap to LEFT JOIN LATERAL ... ON TRUE to preserve her with NULLs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;customer_id&lt;/th&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;order_ts&lt;/th&gt;
&lt;th&gt;total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Alice&lt;/td&gt;
&lt;td&gt;2026-07-09&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Alice&lt;/td&gt;
&lt;td&gt;2026-07-08&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Bob&lt;/td&gt;
&lt;td&gt;2026-07-07&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Bob&lt;/td&gt;
&lt;td&gt;2026-07-02&lt;/td&gt;
&lt;td&gt;70&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;CROSS JOIN LATERAL binds the outer row into the inner query&lt;/strong&gt;&lt;/strong&gt; — inside the parentheses, &lt;code&gt;c.id&lt;/code&gt; is a live value from the current outer row, not a table reference. The planner treats this as a correlated join, not a Cartesian product.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;LIMIT 2 is a per-outer-row limit, not a global limit&lt;/strong&gt;&lt;/strong&gt; — the LATERAL subquery runs &lt;em&gt;once per outer row&lt;/em&gt; and each run applies its own LIMIT. This is why you can't achieve the same result with a plain &lt;code&gt;SELECT ... LIMIT 2&lt;/code&gt; at the top level.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Index seek + bounded scan&lt;/strong&gt;&lt;/strong&gt; — with an index on &lt;code&gt;(customer_id, order_ts DESC)&lt;/code&gt;, each inner run is one seek (position on the customer_id) plus at most two forward reads. Total cost is &lt;code&gt;outer_rows × (seek + 2 × row_fetch)&lt;/code&gt; — dramatically cheaper than sorting all orders.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;CROSS APPLY on SQL Server is a syntactic rebrand&lt;/strong&gt;&lt;/strong&gt; — the semantics are identical to CROSS JOIN LATERAL. The &lt;code&gt;TOP 2&lt;/code&gt; keyword replaces &lt;code&gt;LIMIT 2&lt;/code&gt;; everything else is the same query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;O(N_c × (log N_o + K))&lt;/code&gt; where &lt;code&gt;N_c&lt;/code&gt; is customer count, &lt;code&gt;N_o&lt;/code&gt; is order count, and &lt;code&gt;K = 2&lt;/code&gt; is the LIMIT. Compare with &lt;code&gt;ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY order_ts DESC)&lt;/code&gt;, which is &lt;code&gt;O(N_o log N_o)&lt;/code&gt; for the sort — bad when &lt;code&gt;N_o&lt;/code&gt; is huge and &lt;code&gt;N_c&lt;/code&gt; is small. LATERAL wins on skewed data with a good index; the window function wins when the (customer, order) distribution is uniform and no index is available.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — top-N per group&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Top-N per group problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/top-n" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — joins (SQL)&lt;/span&gt;
&lt;strong&gt;SQL joins — full library&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/joins/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  2. LATERAL syntax &amp;amp; mental model
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;postgres lateral&lt;/code&gt; and &lt;code&gt;lateral vs subquery&lt;/code&gt; — CROSS JOIN LATERAL, LEFT JOIN LATERAL, and the correlation binding that makes it all work
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;LATERAL turns the right-hand FROM-clause subquery into a per-outer-row invocation, letting the inner query freely reference outer columns and producing zero, one, or many rows per outer — behaving like an inner join (CROSS JOIN LATERAL) or a left join (LEFT JOIN LATERAL … ON TRUE) depending on the keyword&lt;/strong&gt;. Once you say "for each outer row, run this inner query and either drop or preserve the outer row based on whether the inner returned anything," the LATERAL syntax interview surface reduces to a two-slot template.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqpufda81evg7c9yplyrl.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqpufda81evg7c9yplyrl.jpeg" alt="Visual diagram of LATERAL syntax and mental model — left an outer table with three rows arrowing individually into a correlated subquery card, right a syntax legend with LEFT JOIN LATERAL preserving outer rows and CROSS JOIN LATERAL dropping empty ones, plus an annotation about correlation binding letting the inner query read outer columns; on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Slot 1 — the join keyword.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CROSS JOIN LATERAL (…) alias&lt;/code&gt; — inner-join semantics. If the inner subquery returns zero rows, the outer row is &lt;em&gt;dropped&lt;/em&gt; from the result. Equivalent to &lt;code&gt;CROSS APPLY&lt;/code&gt; on SQL Server / Oracle.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LEFT JOIN LATERAL (…) alias ON TRUE&lt;/code&gt; — left-outer-join semantics. If the inner subquery returns zero rows, the outer row is &lt;em&gt;preserved&lt;/em&gt; with NULLs in the inner columns. Equivalent to &lt;code&gt;OUTER APPLY&lt;/code&gt; on SQL Server / Oracle.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;INNER JOIN LATERAL (…) alias ON TRUE&lt;/code&gt; — spelled out; identical to CROSS JOIN LATERAL on most engines.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RIGHT JOIN LATERAL&lt;/code&gt; — not defined. LATERAL is asymmetric — the inner references the outer, not vice versa. There's no way to reverse the direction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 2 — the inner subquery.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Any SELECT that can reference the outer columns. Includes CTEs (&lt;code&gt;WITH … SELECT …&lt;/code&gt;), set-returning function calls (&lt;code&gt;SELECT * FROM generate_series(1, outer.n)&lt;/code&gt;), unnest expressions (&lt;code&gt;SELECT * FROM jsonb_array_elements(outer.payload)&lt;/code&gt;), table-valued function calls (Oracle / SQL Server), and window-function subqueries.&lt;/li&gt;
&lt;li&gt;Returns zero, one, or many rows. Multiple columns supported — the LATERAL is a &lt;em&gt;table&lt;/em&gt;, not a scalar.&lt;/li&gt;
&lt;li&gt;Can be aliased — &lt;code&gt;LATERAL (…) AS o&lt;/code&gt; — with column aliases in the ANSI form &lt;code&gt;LATERAL (…) AS o(col1, col2)&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Correlation binding — the rule that makes LATERAL a "join" instead of a "cross product".&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The inner query sees the outer row as if it were a set of parameters. Any column of the outer table can appear inside the LATERAL subquery.&lt;/li&gt;
&lt;li&gt;Without the LATERAL keyword, an ordinary subquery in the FROM clause cannot reference outer columns — a query like &lt;code&gt;FROM customers c, (SELECT * FROM orders WHERE customer_id = c.id) o&lt;/code&gt; is a syntax error on strict parsers.&lt;/li&gt;
&lt;li&gt;The LATERAL keyword tells the parser "this subquery is allowed to see the outer scope." On Postgres, LATERAL is &lt;em&gt;required&lt;/em&gt; whenever the subquery references outer columns from the same FROM clause.&lt;/li&gt;
&lt;li&gt;On Postgres, LATERAL is &lt;em&gt;implicit&lt;/em&gt; on set-returning functions in the FROM clause — &lt;code&gt;SELECT * FROM t, unnest(t.arr)&lt;/code&gt; works without the keyword. Being explicit is the safer style.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Two syntactic forms on Postgres — comma vs JOIN.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Comma form — &lt;code&gt;FROM outer, LATERAL (…) inner&lt;/code&gt; — equivalent to &lt;code&gt;CROSS JOIN LATERAL&lt;/code&gt;. The comma is a cross join by default.&lt;/li&gt;
&lt;li&gt;JOIN form — &lt;code&gt;FROM outer CROSS JOIN LATERAL (…) inner&lt;/code&gt; or &lt;code&gt;FROM outer LEFT JOIN LATERAL (…) inner ON TRUE&lt;/code&gt;. The JOIN form is more explicit and preferred for review clarity.&lt;/li&gt;
&lt;li&gt;Both compile to the same plan. The JOIN form is what most modern style guides recommend.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common newbie mistakes.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Forgetting &lt;code&gt;ON TRUE&lt;/code&gt; on &lt;code&gt;LEFT JOIN LATERAL&lt;/code&gt;. Every &lt;code&gt;LEFT JOIN&lt;/code&gt; needs an &lt;code&gt;ON&lt;/code&gt; clause; the LATERAL subquery has no join column to reference, so the correct spelling is &lt;code&gt;LEFT JOIN LATERAL (…) x ON TRUE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Omitting the LATERAL keyword. Without it, the outer columns are invisible inside the subquery and the query errors out.&lt;/li&gt;
&lt;li&gt;Assuming the inner query is executed once. It runs once per outer row — the whole point of LATERAL.&lt;/li&gt;
&lt;li&gt;Assuming the outer query can see the inner. It can't — the outer projects only its own columns plus whatever the LATERAL alias exposes.&lt;/li&gt;
&lt;li&gt;Placing an aggregate outside the LATERAL block expecting per-outer aggregation. Aggregates outside operate on the joined result set; per-outer aggregation belongs &lt;em&gt;inside&lt;/em&gt; the LATERAL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How LATERAL interacts with GROUP BY and window functions.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GROUP BY outer_col&lt;/code&gt; after a LATERAL join still works — the group is over the joined result set.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ROW_NUMBER() OVER (PARTITION BY outer_col)&lt;/code&gt; after a LATERAL is redundant if the LATERAL already used &lt;code&gt;LIMIT K&lt;/code&gt; internally.&lt;/li&gt;
&lt;li&gt;Aggregates inside the LATERAL (&lt;code&gt;SELECT SUM(x) FROM inner WHERE inner.k = outer.k&lt;/code&gt;) produce one summary row per outer — equivalent to a correlated subquery with better plan predictability.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Set-returning functions inside LATERAL — the JSON / array pattern.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Postgres: &lt;code&gt;SELECT t.id, e.value FROM t, LATERAL jsonb_array_elements(t.payload) e&lt;/code&gt; — one row per (parent, JSON element) pair.&lt;/li&gt;
&lt;li&gt;Postgres: &lt;code&gt;SELECT t.id, u.n FROM t, LATERAL unnest(t.arr) u(n)&lt;/code&gt; — one row per (parent, array element) pair.&lt;/li&gt;
&lt;li&gt;Postgres: &lt;code&gt;SELECT o.id, r FROM orders o, LATERAL generate_series(1, o.qty) r&lt;/code&gt; — one row per (order, series element) — useful for expanding aggregate quantities into rows.&lt;/li&gt;
&lt;li&gt;Snowflake: &lt;code&gt;SELECT t.id, f.value FROM t, LATERAL FLATTEN(input =&amp;gt; t.payload) f&lt;/code&gt; — the VARIANT / JSON unnest primitive.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Interview probes on syntax fluency.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Why CROSS JOIN LATERAL and not just a plain subquery?"&lt;/strong&gt; — the plain subquery in the FROM clause cannot reference outer columns without LATERAL; LATERAL is the keyword that authorises correlation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"When would you switch to LEFT JOIN LATERAL?"&lt;/strong&gt; — whenever you must preserve outer rows even if the inner returns nothing. Reporting "one row per customer, latest order or NULL" is the canonical case.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"What's the difference between LATERAL and a CTE?"&lt;/strong&gt; — a CTE is evaluated once, produces one result set, and joins to the outer table by its own predicates. A LATERAL runs once per outer row and is naturally correlated. CTEs can't express per-row-limited inner queries the way LATERAL can.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Why does Postgres require the keyword but BigQuery not?"&lt;/strong&gt; — Postgres is strict about correlation scope; BigQuery's &lt;code&gt;UNNEST&lt;/code&gt; on an array column in the FROM clause is implicitly LATERAL because the array reference is inherently correlated to its parent row.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — LATERAL vs a lateral-less rewrite
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The clearest way to see what LATERAL adds: try the same query without it. On Postgres the lateral-less form errors out because the inner subquery can't see the outer columns. Writing both side-by-side is the fastest way to internalise the correlation-binding rule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given &lt;code&gt;customers&lt;/code&gt; and &lt;code&gt;orders&lt;/code&gt;, get the most recent order per customer. Write two Postgres queries — one with LATERAL, one attempting the same without LATERAL — and explain why the lateral-less version fails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt; (Same customers and orders as the first section.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- With LATERAL — compiles&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="k"&gt;LATERAL&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;order_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;order_ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
  &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Without LATERAL — ERROR: invalid reference to FROM-clause entry&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;order_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;           &lt;span class="c1"&gt;-- ERROR: no c.id in scope&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;order_ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
  &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The lateral-less version places the subquery in the FROM clause but &lt;em&gt;without&lt;/em&gt; the LATERAL keyword. Postgres treats it as an independent subquery whose scope is only its own FROM clause — &lt;code&gt;c.id&lt;/code&gt; is not visible.&lt;/li&gt;
&lt;li&gt;The parser rejects the query with &lt;code&gt;ERROR: invalid reference to FROM-clause entry for table "c"&lt;/code&gt;. On MySQL 8 the same error appears as &lt;code&gt;Unknown column 'c.id' in 'where clause'&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Adding LATERAL to the second query fixes it — the keyword authorises the correlation.&lt;/li&gt;
&lt;li&gt;Without LATERAL, the equivalent trick is a correlated scalar subquery in the SELECT list or a &lt;code&gt;ROW_NUMBER()&lt;/code&gt; + filter — both work, both compile, both have different perf characteristics.&lt;/li&gt;
&lt;li&gt;The rule of thumb — if your subquery in the FROM clause needs to see the outer row's columns, you need LATERAL. Every time.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;order_ts&lt;/th&gt;
&lt;th&gt;total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Alice&lt;/td&gt;
&lt;td&gt;2026-07-09&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Bob&lt;/td&gt;
&lt;td&gt;2026-07-07&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Carol&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; LATERAL is the keyword that turns a "same-level subquery" into a "correlated join." Without it, subquery scopes are isolated; with it, the inner query sees the outer row like function parameters.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — &lt;code&gt;LEFT JOIN LATERAL ... ON TRUE&lt;/code&gt; for preserving outer rows
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The most common LATERAL variant in production analytics — "one row per customer, plus the latest order or NULLs if there isn't one." The &lt;code&gt;ON TRUE&lt;/code&gt; looks weird until you realise every LEFT JOIN needs an ON clause and there's no natural join column between a LATERAL block and its outer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write a Postgres query that returns every customer along with their most recent order (or NULLs for customers who never ordered). Sort by customer id.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt; (Same customers and orders as above.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;last_order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;last_order_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;last_order_total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="k"&gt;LATERAL&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
  &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;LEFT JOIN LATERAL&lt;/code&gt; — LEFT-outer semantics. If the inner subquery returns zero rows, the outer row is preserved with NULLs in the inner columns.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ON TRUE&lt;/code&gt; — every LEFT JOIN needs an ON clause. The LATERAL has no natural join column (the correlation happens inside the parentheses), so &lt;code&gt;ON TRUE&lt;/code&gt; says "match unconditionally on whatever the inner emits."&lt;/li&gt;
&lt;li&gt;Inner subquery — one row per outer via &lt;code&gt;LIMIT 1&lt;/code&gt;. Reads &lt;code&gt;c.id&lt;/code&gt; from the outer scope.&lt;/li&gt;
&lt;li&gt;Carol has no orders — the inner returns zero rows — the LEFT JOIN LATERAL preserves Carol with NULLs in &lt;code&gt;last_order_id&lt;/code&gt;, &lt;code&gt;last_order_ts&lt;/code&gt;, &lt;code&gt;last_order_total&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Alice and Bob each have orders — the inner returns exactly one row — the LEFT JOIN LATERAL emits the pair.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;customer_id&lt;/th&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;last_order_id&lt;/th&gt;
&lt;th&gt;last_order_ts&lt;/th&gt;
&lt;th&gt;last_order_total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Alice&lt;/td&gt;
&lt;td&gt;104&lt;/td&gt;
&lt;td&gt;2026-07-09&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Bob&lt;/td&gt;
&lt;td&gt;106&lt;/td&gt;
&lt;td&gt;2026-07-07&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Carol&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; &lt;code&gt;LEFT JOIN LATERAL (…) x ON TRUE&lt;/code&gt; is the canonical "outer preserved, inner optional" pattern. Every reporting query with a "most recent event per entity" ask uses this shape.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — LATERAL over a set-returning function
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The killer LATERAL pattern for semi-structured data — unnesting arrays or JSON per outer row. Postgres exposes &lt;code&gt;unnest&lt;/code&gt;, &lt;code&gt;jsonb_array_elements&lt;/code&gt;, &lt;code&gt;generate_series&lt;/code&gt;; Snowflake exposes &lt;code&gt;FLATTEN&lt;/code&gt;; MySQL uses &lt;code&gt;JSON_TABLE&lt;/code&gt;. All read cleanly inside a LATERAL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given &lt;code&gt;orders(id, items jsonb)&lt;/code&gt; where &lt;code&gt;items&lt;/code&gt; is a JSON array of &lt;code&gt;{sku, qty, price}&lt;/code&gt; objects, unroll each item into its own row alongside the parent order id.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;items&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[{"sku":"A","qty":2,"price":10},{"sku":"B","qty":1,"price":25}]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[{"sku":"C","qty":3,"price":5}]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'sku'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'qty'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'price'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="k"&gt;LATERAL&lt;/span&gt; &lt;span class="n"&gt;jsonb_array_elements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;jsonb_array_elements(o.items)&lt;/code&gt; is a set-returning function that produces one row per element of the JSON array — perfect for unnesting.&lt;/li&gt;
&lt;li&gt;Inside the LATERAL, &lt;code&gt;o.items&lt;/code&gt; is bound from the outer row. Order 1 emits two elements; order 2 emits one.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CROSS JOIN LATERAL&lt;/code&gt; — inner-join semantics. Orders with an empty or NULL &lt;code&gt;items&lt;/code&gt; array would be dropped; swap to &lt;code&gt;LEFT JOIN LATERAL … ON TRUE&lt;/code&gt; to preserve them.&lt;/li&gt;
&lt;li&gt;Outside the LATERAL, we destructure each element with &lt;code&gt;-&amp;gt;&amp;gt;&lt;/code&gt; (returns text) and cast to int / numeric as needed.&lt;/li&gt;
&lt;li&gt;Snowflake equivalent: &lt;code&gt;LATERAL FLATTEN(input =&amp;gt; o.items) f&lt;/code&gt; then &lt;code&gt;f.value:sku::string&lt;/code&gt;, &lt;code&gt;f.value:qty::int&lt;/code&gt;. SQL Server: &lt;code&gt;CROSS APPLY OPENJSON(o.items) WITH (sku VARCHAR(50), qty INT, price DECIMAL(10,2))&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;order_id&lt;/th&gt;
&lt;th&gt;sku&lt;/th&gt;
&lt;th&gt;qty&lt;/th&gt;
&lt;th&gt;price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; LATERAL + set-returning function is &lt;em&gt;the&lt;/em&gt; way to unnest arrays and JSON in modern SQL. Every ETL pipeline that ingests semi-structured payloads uses this shape somewhere.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on LATERAL correlation
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Write a Postgres query that, for each customer, returns the customer's id, the total number of orders they've placed, the total spend, and the details of their most recent order in one row. Use LATERAL and explain why this beats three separate correlated subqueries."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using LEFT JOIN LATERAL with per-outer aggregate + top-1
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;                                 &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;agg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;agg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_spend&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;last_ord&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;                          &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;last_order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;last_ord&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt;                    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;last_order_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;last_ord&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;                       &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;last_order_total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="k"&gt;LATERAL&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;order_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_spend&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;agg&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="k"&gt;LATERAL&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;order_ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
  &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;last_ord&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Outer&lt;/th&gt;
&lt;th&gt;agg subquery result&lt;/th&gt;
&lt;th&gt;last_ord subquery result&lt;/th&gt;
&lt;th&gt;Emitted row&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;c1 = Alice&lt;/td&gt;
&lt;td&gt;(4, 215)&lt;/td&gt;
&lt;td&gt;(104, 07-09, 60)&lt;/td&gt;
&lt;td&gt;(1, Alice, 4, 215, 104, 07-09, 60)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;c2 = Bob&lt;/td&gt;
&lt;td&gt;(2, 100)&lt;/td&gt;
&lt;td&gt;(106, 07-07, 30)&lt;/td&gt;
&lt;td&gt;(2, Bob, 2, 100, 106, 07-07, 30)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;c3 = Carol&lt;/td&gt;
&lt;td&gt;(0, 0)&lt;/td&gt;
&lt;td&gt;(NULL, NULL, NULL)&lt;/td&gt;
&lt;td&gt;(3, Carol, 0, 0, NULL, NULL, NULL)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two LATERAL blocks per outer row — one aggregates, the other picks the top-1. Both are LEFT-outer, so customers with zero orders still appear (agg returns (0, 0) from the empty set with COALESCE; last_ord returns zero rows so the LEFT JOIN emits NULLs).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;customer_id&lt;/th&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;order_count&lt;/th&gt;
&lt;th&gt;total_spend&lt;/th&gt;
&lt;th&gt;last_order_id&lt;/th&gt;
&lt;th&gt;last_order_ts&lt;/th&gt;
&lt;th&gt;last_order_total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Alice&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;215&lt;/td&gt;
&lt;td&gt;104&lt;/td&gt;
&lt;td&gt;2026-07-09&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Bob&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;106&lt;/td&gt;
&lt;td&gt;2026-07-07&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Carol&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;One LATERAL block per per-outer computation&lt;/strong&gt;&lt;/strong&gt; — the aggregation lives in one LATERAL, the top-1 lookup lives in another. Each block runs once per outer row with the outer's &lt;code&gt;c.id&lt;/code&gt; bound in. Adding a third computation adds a third LATERAL — the pattern composes cleanly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;COALESCE inside the aggregate LATERAL&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;SUM(total)&lt;/code&gt; on an empty result set is NULL. Wrapping it in &lt;code&gt;COALESCE(..., 0)&lt;/code&gt; turns Carol's spend into a proper zero instead of a NULL. This is idiomatic for "count-plus-sum-with-zero-default" reports.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;LEFT JOIN LATERAL for the top-1 lookup&lt;/strong&gt;&lt;/strong&gt; — Carol has no orders, so the top-1 subquery returns zero rows. LEFT JOIN LATERAL preserves her outer row with NULLs; CROSS JOIN LATERAL would drop her, which is wrong for a "one row per customer" report.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Three separate correlated scalars would be slower&lt;/strong&gt;&lt;/strong&gt; — one &lt;code&gt;(SELECT COUNT(*) FROM orders WHERE …)&lt;/code&gt; in the SELECT list plus one &lt;code&gt;(SELECT SUM(total) FROM …)&lt;/code&gt; plus one &lt;code&gt;(SELECT id FROM … ORDER BY … LIMIT 1)&lt;/code&gt; is three inner passes per outer. Two LATERAL blocks is two inner passes — the aggregate combines COUNT and SUM in one pass — and the plan is more predictable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;O(N_c × (P_agg + P_top1))&lt;/code&gt; where &lt;code&gt;P_agg&lt;/code&gt; is the cost of the aggregate scan per customer (index on &lt;code&gt;(customer_id)&lt;/code&gt; gives &lt;code&gt;O(matching_rows)&lt;/code&gt;) and &lt;code&gt;P_top1&lt;/code&gt; is the cost of the top-1 seek (index on &lt;code&gt;(customer_id, order_ts DESC)&lt;/code&gt; gives &lt;code&gt;O(log N_o)&lt;/code&gt;). With both indexes, the whole query is &lt;code&gt;O(N_c × (matching_rows + log N_o))&lt;/code&gt; — much better than three uncorrelated table scans.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — joins&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Joins problem library&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/joins" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — subqueries&lt;/span&gt;
&lt;strong&gt;Subquery drills&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/subqueries" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  3. CROSS APPLY &amp;amp; OUTER APPLY
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;cross apply sql server&lt;/code&gt; and &lt;code&gt;outer apply&lt;/code&gt; — SQL Server's LATERAL equivalent, the translation table, and TVF composition
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;&lt;code&gt;CROSS APPLY&lt;/code&gt; is SQL Server's spelling of &lt;code&gt;CROSS JOIN LATERAL&lt;/code&gt;, and &lt;code&gt;OUTER APPLY&lt;/code&gt; is its spelling of &lt;code&gt;LEFT JOIN LATERAL ... ON TRUE&lt;/code&gt; — identical semantics with a different keyword, plus a killer composition trick with table-valued functions (TVFs) that turns "run this parametrised query per row" into a first-class citizen&lt;/strong&gt;. Once you know the two-line translation table, every ANSI LATERAL recipe maps cleanly to SQL Server.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy0i7bxqghscs3enfhlzm.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy0i7bxqghscs3enfhlzm.jpeg" alt="Visual diagram of CROSS APPLY vs OUTER APPLY — left a translation table mapping LATERAL keywords to APPLY keywords, centre an APPLY + TVF composition card showing SELECT ... CROSS APPLY top_n_orders(c.id), right a JSON_TABLE + APPLY unnest card; on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The translation table — every ANSI LATERAL maps to an APPLY.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CROSS JOIN LATERAL (…) alias&lt;/code&gt; → &lt;code&gt;CROSS APPLY (…) alias&lt;/code&gt;. Inner-join semantics; drops outer row when inner returns zero.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LEFT JOIN LATERAL (…) alias ON TRUE&lt;/code&gt; → &lt;code&gt;OUTER APPLY (…) alias&lt;/code&gt;. Left-outer semantics; preserves outer row with NULLs when inner returns zero.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SELECT * FROM t, LATERAL (…)&lt;/code&gt; → &lt;code&gt;SELECT * FROM t CROSS APPLY (…)&lt;/code&gt;. Same semantics.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LIMIT K&lt;/code&gt; inside the inner → &lt;code&gt;TOP K&lt;/code&gt; inside the inner (SQL Server uses &lt;code&gt;TOP&lt;/code&gt;, not &lt;code&gt;LIMIT&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;jsonb_array_elements(t.payload)&lt;/code&gt; (Postgres) → &lt;code&gt;OPENJSON(t.payload)&lt;/code&gt; (SQL Server 2016+).&lt;/li&gt;
&lt;li&gt;Set-returning functions → CROSS APPLY of a TVF or of &lt;code&gt;OPENJSON&lt;/code&gt; / &lt;code&gt;STRING_SPLIT&lt;/code&gt; / &lt;code&gt;OPENROWSET&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why SQL Server has APPLY instead of LATERAL.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Microsoft shipped &lt;code&gt;CROSS APPLY&lt;/code&gt; and &lt;code&gt;OUTER APPLY&lt;/code&gt; in SQL Server 2005 — years before Postgres, MySQL, or Oracle added &lt;code&gt;LATERAL&lt;/code&gt;. Once the keyword was baked into the SQL Server dialect, changing it would break every stored procedure ever written.&lt;/li&gt;
&lt;li&gt;Semantically APPLY and LATERAL are equivalent. There is no operation you can do in ANSI LATERAL that you can't do with CROSS APPLY / OUTER APPLY.&lt;/li&gt;
&lt;li&gt;Oracle 12c (2013) shipped both keywords for portability — ANSI LATERAL for standards-compliance and CROSS/OUTER APPLY for teams migrating from SQL Server.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;APPLY + TVF — the composition superpower.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;table-valued function&lt;/strong&gt; (TVF) is a user-defined function that returns a table. SQL Server, Oracle, and Postgres all support them (Postgres calls them SETOF-returning functions).&lt;/li&gt;
&lt;li&gt;The killer pattern — &lt;code&gt;SELECT c.*, t.* FROM customers c CROSS APPLY dbo.top_n_orders(c.id, 3) t&lt;/code&gt; — invokes the TVF once per outer row, passing the outer's &lt;code&gt;c.id&lt;/code&gt; and a fixed &lt;code&gt;3&lt;/code&gt; as arguments. The TVF returns a table; the outer row is joined to every row it emits.&lt;/li&gt;
&lt;li&gt;This turns a piece of reusable business logic (&lt;code&gt;top_n_orders&lt;/code&gt;) into a plug-and-play building block. Callers get the correlated behaviour for free.&lt;/li&gt;
&lt;li&gt;Postgres equivalent — &lt;code&gt;LATERAL (SELECT * FROM top_n_orders(c.id, 3))&lt;/code&gt; or, if the function is &lt;code&gt;SETOF orders&lt;/code&gt;, just &lt;code&gt;LATERAL top_n_orders(c.id, 3) t&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;JSON unnest via APPLY on SQL Server.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;OPENJSON(json_column)&lt;/code&gt; is SQL Server's JSON parser. Called inside CROSS APPLY, it returns one row per JSON element / property, with columns you specify via &lt;code&gt;WITH (…)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CROSS APPLY OPENJSON(o.items) WITH (sku VARCHAR(50), qty INT, price DECIMAL(10,2))&lt;/code&gt; — reads exactly like Postgres's &lt;code&gt;jsonb_array_elements&lt;/code&gt; with per-column extraction.&lt;/li&gt;
&lt;li&gt;For older SQL Server (pre-2016), the equivalent is a custom split TVF or &lt;code&gt;STUFF + FOR XML PATH&lt;/code&gt; string manipulation — much uglier.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;OPENJSON / OPENROWSET / STRING_SPLIT with APPLY — the SQL Server unnest family.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;OPENJSON(json)&lt;/code&gt; — unnest a JSON string or JSON column. Returns key/value pairs by default; with &lt;code&gt;WITH (…)&lt;/code&gt; returns typed columns.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;STRING_SPLIT(delimited_string, delimiter)&lt;/code&gt; — unnest a delimited string. Returns one row per token.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;OPENROWSET(BULK 'file.csv', ...)&lt;/code&gt; — read an external file. Not a LATERAL primitive per se, but often composed with APPLY.&lt;/li&gt;
&lt;li&gt;All three are commonly used with CROSS APPLY to unnest a column into rows per parent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Oracle's LATERAL + APPLY duality.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Oracle 12c ships both keywords. &lt;code&gt;LATERAL (…)&lt;/code&gt; and &lt;code&gt;CROSS APPLY (…)&lt;/code&gt; are interchangeable; &lt;code&gt;LEFT JOIN LATERAL (…) ON TRUE&lt;/code&gt; and &lt;code&gt;OUTER APPLY (…)&lt;/code&gt; are also interchangeable.&lt;/li&gt;
&lt;li&gt;Most Oracle style guides prefer LATERAL for ANSI-conformance and use APPLY only when porting SQL Server code.&lt;/li&gt;
&lt;li&gt;Oracle also supports &lt;code&gt;TABLE(function_name(args))&lt;/code&gt; inside a FROM clause — a legacy syntax for calling PL/SQL table functions, superseded by the LATERAL / APPLY approach.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common APPLY gotchas.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;TOP N&lt;/code&gt; inside CROSS APPLY without &lt;code&gt;ORDER BY&lt;/code&gt; — non-deterministic. Always pair &lt;code&gt;TOP N&lt;/code&gt; with &lt;code&gt;ORDER BY&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Forgetting to include the outer alias — &lt;code&gt;CROSS APPLY (SELECT TOP 3 * FROM orders WHERE customer_id = c.id ORDER BY order_ts DESC) o&lt;/code&gt; — the reference to &lt;code&gt;c.id&lt;/code&gt; must live inside the parentheses, not at the top level.&lt;/li&gt;
&lt;li&gt;Using &lt;code&gt;SELECT *&lt;/code&gt; inside APPLY that returns overlapping column names — SQL Server may error on ambiguous columns.&lt;/li&gt;
&lt;li&gt;Using APPLY in an old compatibility level — APPLY is available from 2005 onwards, so any modern SQL Server supports it, but SQL Server 2000 databases upgraded in-place without changing compatibility level may still have issues.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Perf story — APPLY vs alternatives on SQL Server.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;APPLY is often faster than &lt;code&gt;WITH cte AS (… ROW_NUMBER() OVER (…)) SELECT WHERE rn &amp;lt;= K&lt;/code&gt; when a covering index on &lt;code&gt;(partition_col, order_col)&lt;/code&gt; exists. The APPLY plan seeks per outer row and stops early; the CTE plan sorts every row.&lt;/li&gt;
&lt;li&gt;APPLY beats correlated scalar subquery when you need multiple columns from the inner query.&lt;/li&gt;
&lt;li&gt;APPLY loses to a plain hash join when the join is a simple equality on a bounded inner set — the planner picks hash by default anyway.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Which APPLY variant when.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CROSS APPLY&lt;/code&gt; — use when you want inner-join semantics. Drop outer rows with no matching inner rows.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;OUTER APPLY&lt;/code&gt; — use for reporting queries that need "one row per outer, plus optional inner." Every "customer report with most-recent-order-or-NULL" query uses OUTER APPLY.&lt;/li&gt;
&lt;li&gt;Never use &lt;code&gt;CROSS APPLY&lt;/code&gt; when you need to preserve outer rows — you'll silently lose data.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — LATERAL → CROSS APPLY translation of the top-3-orders query
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The most common port — take a working Postgres LATERAL query and rewrite it for SQL Server. The translation is mechanical.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Rewrite the top-3-orders-per-customer LATERAL query as SQL Server CROSS APPLY.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt; (Same customers and orders as before.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Postgres: LATERAL&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="k"&gt;LATERAL&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;order_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;order_ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
  &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- SQL Server: CROSS APPLY&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="n"&gt;APPLY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;TOP&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="n"&gt;order_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;order_ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keyword swap — &lt;code&gt;CROSS JOIN LATERAL&lt;/code&gt; becomes &lt;code&gt;CROSS APPLY&lt;/code&gt;. The parentheses stay the same.&lt;/li&gt;
&lt;li&gt;Limit keyword — &lt;code&gt;LIMIT 3&lt;/code&gt; becomes &lt;code&gt;TOP 3&lt;/code&gt; at the beginning of the SELECT. SQL Server has no &lt;code&gt;LIMIT&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ORDER BY&lt;/code&gt; inside the inner query stays. SQL Server requires it for deterministic &lt;code&gt;TOP&lt;/code&gt; results.&lt;/li&gt;
&lt;li&gt;Correlation binding — the reference to &lt;code&gt;c.id&lt;/code&gt; inside the inner query works identically. APPLY authorises correlation just like LATERAL.&lt;/li&gt;
&lt;li&gt;Outer &lt;code&gt;ORDER BY&lt;/code&gt; at the end works the same way.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt; (Same as the LATERAL version.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Every LATERAL query maps to CROSS APPLY / OUTER APPLY with two keyword swaps. Keep the same mental model; change only the words.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — CROSS APPLY + inline TVF for reusable per-row logic
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The composition win — wrap the per-row logic in a TVF, then compose it via CROSS APPLY. Every caller gets the correlated behaviour with one keyword.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Create an inline TVF &lt;code&gt;dbo.top_n_orders(@cust INT, @n INT)&lt;/code&gt; that returns the top-N most-recent orders for a customer, then use CROSS APPLY to fetch the top-3 for every customer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt; (Same as before.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- TVF definition (SQL Server 2008+)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;top_n_orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;cust&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;RETURN&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;TOP&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;cust&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;order_ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Caller uses CROSS APPLY to invoke the TVF per outer row&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="n"&gt;APPLY&lt;/span&gt; &lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;top_n_orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;CREATE FUNCTION … RETURNS TABLE AS RETURN &amp;lt;SELECT&amp;gt;&lt;/code&gt; defines an &lt;strong&gt;inline TVF&lt;/strong&gt; — the body is a single SELECT that returns a table.&lt;/li&gt;
&lt;li&gt;Inline TVFs are optimised alongside the caller — the planner inlines the function body and rewrites it as if you'd typed the SELECT inline. No function-call overhead.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CROSS APPLY dbo.top_n_orders(c.id, 3) t&lt;/code&gt; — for each customer row, invoke the TVF with the outer's &lt;code&gt;c.id&lt;/code&gt; and the constant &lt;code&gt;3&lt;/code&gt;. The TVF returns the top-3 orders; APPLY joins them to the outer row.&lt;/li&gt;
&lt;li&gt;Downstream callers use one keyword — &lt;code&gt;CROSS APPLY top_n_orders(...)&lt;/code&gt; — instead of duplicating the ORDER BY + TOP + WHERE. Business logic centralised.&lt;/li&gt;
&lt;li&gt;Compare with multi-statement TVFs (&lt;code&gt;RETURNS @t TABLE(...) AS BEGIN INSERT INTO @t SELECT ... END&lt;/code&gt;) — these are NOT inlined and have significantly worse perf. Prefer inline TVFs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;customer_id&lt;/th&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;order_id&lt;/th&gt;
&lt;th&gt;order_ts&lt;/th&gt;
&lt;th&gt;total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Alice&lt;/td&gt;
&lt;td&gt;104&lt;/td&gt;
&lt;td&gt;2026-07-09&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Alice&lt;/td&gt;
&lt;td&gt;103&lt;/td&gt;
&lt;td&gt;2026-07-08&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Alice&lt;/td&gt;
&lt;td&gt;102&lt;/td&gt;
&lt;td&gt;2026-07-05&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Bob&lt;/td&gt;
&lt;td&gt;106&lt;/td&gt;
&lt;td&gt;2026-07-07&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Bob&lt;/td&gt;
&lt;td&gt;105&lt;/td&gt;
&lt;td&gt;2026-07-02&lt;/td&gt;
&lt;td&gt;70&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Inline TVFs + CROSS APPLY is the SQL Server pattern for reusable per-row logic. One function definition, N callers, zero copy-paste.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — OPENJSON unnest with CROSS APPLY
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The JSON unnest story on SQL Server. &lt;code&gt;OPENJSON&lt;/code&gt; is a table-valued function that parses JSON and emits rows; CROSS APPLY runs it per parent row.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given &lt;code&gt;orders(id, items NVARCHAR(MAX))&lt;/code&gt; where &lt;code&gt;items&lt;/code&gt; is a JSON array of &lt;code&gt;{sku, qty, price}&lt;/code&gt; objects, unroll each item into its own row.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;items&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[{"sku":"A","qty":2,"price":10},{"sku":"B","qty":1,"price":25}]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[{"sku":"C","qty":3,"price":5}]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;       &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="n"&gt;APPLY&lt;/span&gt; &lt;span class="n"&gt;OPENJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;sku&lt;/span&gt;   &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="s1"&gt;'$.sku'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;qty&lt;/span&gt;   &lt;span class="nb"&gt;INT&lt;/span&gt;             &lt;span class="s1"&gt;'$.qty'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="s1"&gt;'$.price'&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;OPENJSON(o.items)&lt;/code&gt; parses the JSON string in &lt;code&gt;o.items&lt;/code&gt; and returns one row per element of the top-level array.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WITH (col type '$.path', ...)&lt;/code&gt; — the "explicit-schema" form of OPENJSON. Each column extracts a JSON path from the current element. Types are enforced.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CROSS APPLY&lt;/code&gt; runs OPENJSON once per outer order row, passing &lt;code&gt;o.items&lt;/code&gt; in. Order 1 emits two element rows; order 2 emits one.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WITH&lt;/code&gt; is important — without it, OPENJSON returns a key/value/type triplet per element, which is much harder to use. Always use the explicit-schema form for typed unnest.&lt;/li&gt;
&lt;li&gt;The equivalent on Postgres — &lt;code&gt;jsonb_array_elements(o.items)&lt;/code&gt; in LATERAL, then &lt;code&gt;-&amp;gt;&amp;gt; 'sku'&lt;/code&gt; etc. Snowflake — &lt;code&gt;LATERAL FLATTEN(input =&amp;gt; o.items)&lt;/code&gt; then &lt;code&gt;f.value:sku::string&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;order_id&lt;/th&gt;
&lt;th&gt;sku&lt;/th&gt;
&lt;th&gt;qty&lt;/th&gt;
&lt;th&gt;price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;10.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;25.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;5.00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; &lt;code&gt;CROSS APPLY OPENJSON(json_col) WITH (col type '$.path', ...)&lt;/code&gt; is &lt;em&gt;the&lt;/em&gt; SQL Server pattern for JSON unnest. Every ETL that lands JSON payloads uses this shape.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on APPLY composition
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Design a reporting query on SQL Server that, for each product, returns the top-3 orders by revenue and the total revenue across all orders. Use CROSS APPLY for the top-3, an inline TVF for reuse, and OUTER APPLY for the aggregate so products with no orders still appear."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using inline TVF + CROSS APPLY + OUTER APPLY composition
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Inline TVF for top-N by revenue&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;top_n_orders_by_rev&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;prod&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;RETURN&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;TOP&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;revenue&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;order_lines&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;product_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;prod&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Reporting query&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;            &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;          &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;product_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;            &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;top_order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt;      &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;top_order_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;revenue&lt;/span&gt;       &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;top_order_revenue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;agg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_rev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;agg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_count&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
&lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="n"&gt;APPLY&lt;/span&gt; &lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;top_n_orders_by_rev&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;OUTER&lt;/span&gt; &lt;span class="n"&gt;APPLY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;order_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qty&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_rev&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;order_lines&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;product_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;agg&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;revenue&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Outer (product_id)&lt;/th&gt;
&lt;th&gt;CROSS APPLY t&lt;/th&gt;
&lt;th&gt;OUTER APPLY agg&lt;/th&gt;
&lt;th&gt;Emitted rows&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;p1&lt;/td&gt;
&lt;td&gt;top 3 = (o11, o12, o13)&lt;/td&gt;
&lt;td&gt;(order_count=8, total_rev=520)&lt;/td&gt;
&lt;td&gt;3 rows joined with the agg per row&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;p2&lt;/td&gt;
&lt;td&gt;top 3 = (o21, o22) (only 2 orders)&lt;/td&gt;
&lt;td&gt;(order_count=2, total_rev=90)&lt;/td&gt;
&lt;td&gt;2 rows joined&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;p3&lt;/td&gt;
&lt;td&gt;top 3 = (empty)&lt;/td&gt;
&lt;td&gt;(order_count=0, total_rev=0)&lt;/td&gt;
&lt;td&gt;0 rows (CROSS APPLY drops)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The CROSS APPLY t drops products with no orders — desirable for the "top order per product" report. The OUTER APPLY agg would preserve the outer row if it were the only join; since it's paired with CROSS APPLY t, the outer row is only emitted when t emits at least one inner row.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;product_id&lt;/th&gt;
&lt;th&gt;product_name&lt;/th&gt;
&lt;th&gt;top_order_id&lt;/th&gt;
&lt;th&gt;top_order_ts&lt;/th&gt;
&lt;th&gt;top_order_revenue&lt;/th&gt;
&lt;th&gt;total_rev&lt;/th&gt;
&lt;th&gt;order_count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Widget A&lt;/td&gt;
&lt;td&gt;o11&lt;/td&gt;
&lt;td&gt;2026-07-10&lt;/td&gt;
&lt;td&gt;250&lt;/td&gt;
&lt;td&gt;520&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Widget A&lt;/td&gt;
&lt;td&gt;o12&lt;/td&gt;
&lt;td&gt;2026-07-08&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;td&gt;520&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Widget A&lt;/td&gt;
&lt;td&gt;o13&lt;/td&gt;
&lt;td&gt;2026-07-05&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;520&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Widget B&lt;/td&gt;
&lt;td&gt;o21&lt;/td&gt;
&lt;td&gt;2026-07-09&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Widget B&lt;/td&gt;
&lt;td&gt;o22&lt;/td&gt;
&lt;td&gt;2026-07-06&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Inline TVF centralises the "top N by revenue" logic&lt;/strong&gt;&lt;/strong&gt; — one function definition, called from every reporting query that needs the pattern. The planner inlines the body, so there's zero function-call overhead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;CROSS APPLY for the top-N, OUTER APPLY for the aggregate&lt;/strong&gt;&lt;/strong&gt; — CROSS APPLY drops products with zero orders (they shouldn't appear in a "top orders per product" report); OUTER APPLY preserves the outer row if only the aggregate matters. Choosing between the two is a semantic call, not a syntactic one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;COALESCE(SUM(...), 0) inside the aggregate APPLY&lt;/strong&gt;&lt;/strong&gt; — SUM over an empty set is NULL. Wrapping in COALESCE yields a proper zero, so downstream reports don't need to handle NULL revenues.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Two APPLY blocks compose cleanly&lt;/strong&gt;&lt;/strong&gt; — each block runs once per outer row with the outer's &lt;code&gt;p.id&lt;/code&gt; bound in. Adding a third computation (say, "average order size per product") adds a third block with no side effects on the first two.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;O(N_p × (P_top3 + P_agg))&lt;/code&gt; where &lt;code&gt;P_top3&lt;/code&gt; is the cost of the top-3 seek (with an index on &lt;code&gt;(product_id, revenue DESC)&lt;/code&gt; it's &lt;code&gt;O(log N_o + 3)&lt;/code&gt;) and &lt;code&gt;P_agg&lt;/code&gt; is the cost of the aggregate scan (with an index on &lt;code&gt;(product_id)&lt;/code&gt; it's &lt;code&gt;O(matching_rows)&lt;/code&gt;). Both indexes present, this is fast — dramatically better than doing two separate correlated scans in the SELECT list, and beats a &lt;code&gt;ROW_NUMBER() OVER + WHERE + GROUP BY&lt;/code&gt; alternative on skewed data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — joins (SQL) — hard&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Hard SQL joins&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/joins/sql/hard" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — subquery&lt;/span&gt;
&lt;strong&gt;Subquery mastery&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/subquery" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  4. Top-N per group pattern
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;sql top-n per group&lt;/code&gt; — the LATERAL + LIMIT recipe versus ROW_NUMBER() OVER, and the index shape that makes it fly
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;top-N-per-group is the archetype LATERAL query, and it beats the classic &lt;code&gt;ROW_NUMBER() OVER (PARTITION BY group_col ORDER BY order_col DESC) WHERE rn ≤ N&lt;/code&gt; plan whenever the (group_col, order_col DESC) index exists and N is small — because the LATERAL plan is a nested loop with a per-group LIMIT, and the window function is a full sort&lt;/strong&gt;. Once you know the two shapes and their index requirements, "give me the latest K events per entity" becomes a mechanical choice, not an art.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftco0wbpjcltw4ymzd35f.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftco0wbpjcltw4ymzd35f.jpeg" alt="Visual diagram of top-N-per-group with LATERAL — left a customers table, centre a per-customer LATERAL subquery emitting the last 3 orders each, right a benchmark strip comparing LATERAL + LIMIT with index seek vs ROW_NUMBER OVER with sort + rank; on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three canonical approaches to top-N-per-group.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LATERAL + LIMIT&lt;/strong&gt; — &lt;code&gt;FROM outer CROSS JOIN LATERAL (SELECT ... FROM inner WHERE inner.k = outer.k ORDER BY inner.ts DESC LIMIT N) x&lt;/code&gt;. Plan is nested loop with a bounded inner. Best when (outer.k, inner.ts DESC) is indexed and N is small.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ROW_NUMBER() + filter&lt;/strong&gt; — &lt;code&gt;WITH r AS (SELECT ..., ROW_NUMBER() OVER (PARTITION BY k ORDER BY ts DESC) AS rn FROM inner) SELECT * FROM r WHERE rn &amp;lt;= N&lt;/code&gt;. Plan sorts every row, ranks, then filters. Best when index is missing or N is close to &lt;code&gt;inner_rows / outer_rows&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correlated scalar subquery per column&lt;/strong&gt; — &lt;code&gt;SELECT (SELECT MAX(x) FROM inner WHERE inner.k = outer.k) FROM outer&lt;/code&gt;. Only for single-column-single-row lookups. Cannot express "top 3."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why LATERAL wins with the right index.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;With an index on &lt;code&gt;(customer_id, order_ts DESC)&lt;/code&gt;, the inner query is a &lt;strong&gt;B-tree seek&lt;/strong&gt; to the customer_id + a &lt;strong&gt;bounded scan&lt;/strong&gt; of the next N rows. Cost per outer row is &lt;code&gt;O(log N_inner + N)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The window function has no way to stop early — it must sort all rows in the inner table (or all rows within each partition, which is asymptotically the same) before applying the filter. Cost is &lt;code&gt;O(N_inner log N_inner)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;On a table with 10M orders and 100K customers, LATERAL + LIMIT 3 is roughly &lt;code&gt;100K × (log 10M + 3)&lt;/code&gt; ≈ &lt;code&gt;100K × 27&lt;/code&gt; ≈ &lt;code&gt;2.7M ops&lt;/code&gt;. The window function is &lt;code&gt;10M × log 10M&lt;/code&gt; ≈ &lt;code&gt;10M × 23&lt;/code&gt; ≈ &lt;code&gt;230M ops&lt;/code&gt;. Two orders of magnitude difference — real, reproducible, unmistakable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When ROW_NUMBER() wins.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No index available.&lt;/strong&gt; If you can't add &lt;code&gt;(group_col, order_col DESC)&lt;/code&gt;, LATERAL loses — every inner query becomes a full sequential scan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;N is close to inner_rows / outer_rows.&lt;/strong&gt; If you're asking for "top 100 out of an average 105," you may as well sort everything anyway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-DB portability with legacy engines.&lt;/strong&gt; MySQL 5.7 lacks LATERAL; ROW_NUMBER() is the portable answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-window computations in the same pass.&lt;/strong&gt; If you also need &lt;code&gt;RANK() OVER&lt;/code&gt; and &lt;code&gt;PERCENT_RANK() OVER&lt;/code&gt; in the same query, ROW_NUMBER() piggybacks on the existing sort.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;DISTINCT ON&lt;/code&gt; shortcut (Postgres only).&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Postgres has &lt;code&gt;SELECT DISTINCT ON (customer_id) * FROM orders ORDER BY customer_id, order_ts DESC&lt;/code&gt; — returns exactly one row per customer_id, the first one in the sort order.&lt;/li&gt;
&lt;li&gt;Great for &lt;strong&gt;top-1&lt;/strong&gt; per group. Doesn't extend to top-N (you'd need N passes).&lt;/li&gt;
&lt;li&gt;LATERAL is more general and reads more clearly across the top-1 → top-N spectrum.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;QUALIFY&lt;/code&gt; shortcut (Snowflake / BigQuery / Databricks).&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;SELECT * FROM orders QUALIFY ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY order_ts DESC) &amp;lt;= 3&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Same plan as &lt;code&gt;WITH ... WHERE rn &amp;lt;= 3&lt;/code&gt;, but reads in one clause.&lt;/li&gt;
&lt;li&gt;Portable across Snowflake / BigQuery / Databricks / Teradata; not available on Postgres / MySQL / SQL Server / Oracle.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Skewed-distribution matters — the "hot key" problem.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If 90% of orders belong to 10% of customers, ROW_NUMBER() OVER still has to sort all 100% of orders. LATERAL runs its bounded scan only against each customer's slice — the 10% of hot customers get slightly more expensive scans, but the 90% of cold customers get near-zero cost.&lt;/li&gt;
&lt;li&gt;On heavy-tailed data (which is 99% of real production workloads), LATERAL's per-group cost adapts naturally.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;BigQuery — no LATERAL keyword, but the pattern exists.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;BigQuery uses &lt;code&gt;ARRAY_AGG(... ORDER BY ... LIMIT N)&lt;/code&gt; inside a subquery — a materialisation-heavy alternative.&lt;/li&gt;
&lt;li&gt;Or the QUALIFY shortcut with &lt;code&gt;ROW_NUMBER()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Neither is as clean as ANSI LATERAL, but the semantics are the same.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Ties — a subtle correctness issue.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ORDER BY order_ts DESC LIMIT 3&lt;/code&gt; returns exactly 3 rows even when there are ties on &lt;code&gt;order_ts&lt;/code&gt;. The choice among tied rows is non-deterministic.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ROW_NUMBER() OVER (…) &amp;lt;= 3&lt;/code&gt; behaves identically — non-deterministic tie-breaking.&lt;/li&gt;
&lt;li&gt;For deterministic top-N with ties, use &lt;code&gt;RANK() &amp;lt;= 3&lt;/code&gt; (returns all tied rows) or add a tie-breaker column to the &lt;code&gt;ORDER BY&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Benchmark shape — the shape every senior candidate should know.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Setup: 10M orders, 100K customers, index on &lt;code&gt;(customer_id, order_ts DESC)&lt;/code&gt;, K = 3.&lt;/li&gt;
&lt;li&gt;LATERAL + LIMIT 3: ≈ 1.2s. Plan: nested loop join + index seek + LIMIT.&lt;/li&gt;
&lt;li&gt;ROW_NUMBER() + WHERE rn ≤ 3: ≈ 4.8s. Plan: index scan + sort + filter.&lt;/li&gt;
&lt;li&gt;Correlated scalar per column: ≈ 9.5s (three passes). Plan: N × subquery.&lt;/li&gt;
&lt;li&gt;Ratio: LATERAL is 4× faster than window function, 8× faster than correlated scalar.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — latest 3 orders per customer with LATERAL
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The archetype from an interview lens. Given the standard schema and the standard index, write the LATERAL query and explain the plan.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given &lt;code&gt;orders(id, customer_id, order_ts, total)&lt;/code&gt; with an index on &lt;code&gt;(customer_id, order_ts DESC)&lt;/code&gt;, write a Postgres query returning the three most recent orders per customer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt; (10M orders across 100K customers.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="k"&gt;LATERAL&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;order_ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
  &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Outer scan of &lt;code&gt;customers&lt;/code&gt; — one row per customer. Cost is &lt;code&gt;O(N_c)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Per customer, an index seek into &lt;code&gt;orders&lt;/code&gt; using the &lt;code&gt;(customer_id, order_ts DESC)&lt;/code&gt; index. Cost &lt;code&gt;O(log N_o)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Fetch the next 3 rows sequentially from the index — the ORDER BY is satisfied by the index, so no sort is needed. Cost &lt;code&gt;O(3)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;LATERAL emits three rows per customer (or fewer, for customers with fewer than three orders). Total inner cost &lt;code&gt;O(N_c × (log N_o + 3))&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Final outer ORDER BY is a small sort over &lt;code&gt;~3 × N_c&lt;/code&gt; rows — cheap.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt; (First few rows.)&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;order_id&lt;/th&gt;
&lt;th&gt;order_ts&lt;/th&gt;
&lt;th&gt;total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Alice&lt;/td&gt;
&lt;td&gt;104&lt;/td&gt;
&lt;td&gt;2026-07-09&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Alice&lt;/td&gt;
&lt;td&gt;103&lt;/td&gt;
&lt;td&gt;2026-07-08&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Alice&lt;/td&gt;
&lt;td&gt;102&lt;/td&gt;
&lt;td&gt;2026-07-05&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Bob&lt;/td&gt;
&lt;td&gt;106&lt;/td&gt;
&lt;td&gt;2026-07-07&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Bob&lt;/td&gt;
&lt;td&gt;105&lt;/td&gt;
&lt;td&gt;2026-07-02&lt;/td&gt;
&lt;td&gt;70&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; LATERAL + LIMIT 3 + covering index is the canonical top-N-per-group plan on Postgres, MySQL 8, and Oracle. When (partition_col, order_col DESC) is indexed, this is dramatically faster than any window-function alternative.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — latest 3 orders per customer with ROW_NUMBER()
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The portable alternative. Works everywhere but has to sort every row.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Rewrite the top-3 query using &lt;code&gt;ROW_NUMBER() OVER&lt;/code&gt; without LATERAL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt; (Same as above.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;ranked&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;order_ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;ranked&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rn&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The CTE &lt;code&gt;ranked&lt;/code&gt; computes &lt;code&gt;ROW_NUMBER()&lt;/code&gt; over the whole &lt;code&gt;orders&lt;/code&gt; table, partitioned by &lt;code&gt;customer_id&lt;/code&gt; and ordered by &lt;code&gt;order_ts DESC&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The window function requires a &lt;strong&gt;sort&lt;/strong&gt; on the &lt;code&gt;(customer_id, order_ts DESC)&lt;/code&gt; composite key. If the index covers both columns, the planner &lt;em&gt;may&lt;/em&gt; use an index scan without an explicit sort — but not all planners do.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WHERE r.rn &amp;lt;= 3&lt;/code&gt; filters to the top 3 per customer. This happens &lt;em&gt;after&lt;/em&gt; the window function has ranked every row.&lt;/li&gt;
&lt;li&gt;The outer JOIN to &lt;code&gt;customers&lt;/code&gt; re-attaches the customer name.&lt;/li&gt;
&lt;li&gt;Cost &lt;code&gt;O(N_o log N_o)&lt;/code&gt; for the sort, plus &lt;code&gt;O(N_o)&lt;/code&gt; for the ranking pass. On 10M orders, this is roughly &lt;code&gt;10M × 23&lt;/code&gt; ≈ &lt;code&gt;230M ops&lt;/code&gt; — 2 orders of magnitude worse than the LATERAL plan.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt; (Same rows as the LATERAL version.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; &lt;code&gt;ROW_NUMBER() OVER (… PARTITION BY g ORDER BY o DESC) &amp;lt;= N&lt;/code&gt; is portable across every major engine (except MySQL 5.7 which lacks window functions). Use it when LATERAL is unavailable or when N is close to &lt;code&gt;inner_rows / outer_rows&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — QUALIFY variant on Snowflake / BigQuery
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The one-clause shortcut on modern cloud warehouses. Reads better than the CTE + WHERE combo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Rewrite the top-3-per-customer query using &lt;code&gt;QUALIFY&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt; (Same as above.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Snowflake / BigQuery / Databricks / Teradata&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="n"&gt;QUALIFY&lt;/span&gt; &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;QUALIFY&lt;/code&gt; filters on window-function results — a shorthand for the CTE + WHERE combo.&lt;/li&gt;
&lt;li&gt;Semantically identical to the ROW_NUMBER() + WHERE plan — same sort, same filter, same output.&lt;/li&gt;
&lt;li&gt;Snowflake, BigQuery, Databricks, and Teradata support QUALIFY; Postgres, MySQL, SQL Server, and Oracle do not.&lt;/li&gt;
&lt;li&gt;On engines that support both, QUALIFY reads better; on engines that support only one, LATERAL wins on perf.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt; (Same rows.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; QUALIFY is syntactic sugar for the CTE + WHERE pattern. It doesn't change the plan, but it does reduce the query from three CTEs to one — a real readability win on complex reports.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on top-N-per-group perf
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Design the query to fetch the latest 3 orders per customer on Postgres. Explain when you'd reach for LATERAL, when for &lt;code&gt;ROW_NUMBER() OVER&lt;/code&gt;, and what index you'd add to make the LATERAL plan sing."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using CROSS JOIN LATERAL with the covering index
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Index (usually already exists on production DBs)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;idx_orders_customer_ts_desc&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Query&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;                                     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;                                     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rn_per_customer&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="k"&gt;LATERAL&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;order_ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
  &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Outer (customer_id)&lt;/th&gt;
&lt;th&gt;Inner scan&lt;/th&gt;
&lt;th&gt;Rows emitted&lt;/th&gt;
&lt;th&gt;rn_per_customer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;c1 = Alice&lt;/td&gt;
&lt;td&gt;seek + 3 rows&lt;/td&gt;
&lt;td&gt;(o104, 07-09, 60), (o103, 07-08, 90), (o102, 07-05, 25)&lt;/td&gt;
&lt;td&gt;1, 2, 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;c2 = Bob&lt;/td&gt;
&lt;td&gt;seek + 2 rows&lt;/td&gt;
&lt;td&gt;(o106, 07-07, 30), (o105, 07-02, 70)&lt;/td&gt;
&lt;td&gt;1, 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;c3 = Carol&lt;/td&gt;
&lt;td&gt;seek + 0 rows&lt;/td&gt;
&lt;td&gt;(dropped — CROSS JOIN LATERAL)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The outer scans customers once. Per customer, the inner does one index seek + one bounded scan of up to 3 rows. The optional &lt;code&gt;ROW_NUMBER() OVER (PARTITION BY c.id …)&lt;/code&gt; adds a within-per-customer ordering label — useful for downstream reports that want "1st most recent," "2nd most recent," "3rd most recent."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;customer_id&lt;/th&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;order_id&lt;/th&gt;
&lt;th&gt;order_ts&lt;/th&gt;
&lt;th&gt;total&lt;/th&gt;
&lt;th&gt;rn_per_customer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Alice&lt;/td&gt;
&lt;td&gt;104&lt;/td&gt;
&lt;td&gt;2026-07-09&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Alice&lt;/td&gt;
&lt;td&gt;103&lt;/td&gt;
&lt;td&gt;2026-07-08&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Alice&lt;/td&gt;
&lt;td&gt;102&lt;/td&gt;
&lt;td&gt;2026-07-05&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Bob&lt;/td&gt;
&lt;td&gt;106&lt;/td&gt;
&lt;td&gt;2026-07-07&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Bob&lt;/td&gt;
&lt;td&gt;105&lt;/td&gt;
&lt;td&gt;2026-07-02&lt;/td&gt;
&lt;td&gt;70&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Covering index on (customer_id, order_ts DESC)&lt;/strong&gt;&lt;/strong&gt; — the LATERAL plan can seek the index to the exact customer, then walk forward for up to three rows in the correct order. No sort, no filter, no rescanning the table. This is the single most important perf lever for LATERAL top-N.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;LIMIT 3 inside the LATERAL is a per-outer-row bound&lt;/strong&gt;&lt;/strong&gt; — the inner scan stops after emitting three rows. Compared to &lt;code&gt;WHERE rn &amp;lt;= 3&lt;/code&gt;, which needs the window function to rank &lt;em&gt;every&lt;/em&gt; row before filtering, the LATERAL plan pays exactly &lt;code&gt;3&lt;/code&gt; inner-row reads per customer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;CROSS JOIN LATERAL for the top-N semantics&lt;/strong&gt;&lt;/strong&gt; — customers with zero orders should not appear in the report (they have no "latest 3"). CROSS drops them; swap to LEFT JOIN LATERAL ... ON TRUE if the report demands "customers with no orders shown as zeros."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;ROW_NUMBER() OVER runs over the LATERAL output, not the base table&lt;/strong&gt;&lt;/strong&gt; — because the LATERAL already limited to 3 rows per customer, the window function operates on at most &lt;code&gt;3 × N_customers&lt;/code&gt; rows. Free labels, no extra sort.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — with the covering index, &lt;code&gt;O(N_c × (log N_o + K))&lt;/code&gt; where &lt;code&gt;K = 3&lt;/code&gt;. On 10M orders × 100K customers, this is ≈ 2.7M ops — sub-second on any modern database. Without the index, the plan degrades to &lt;code&gt;O(N_c × N_o)&lt;/code&gt; = O(10^12) — unrun-able. The index is the whole game.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — top-N per group&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Top-N per group drills&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/top-n" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — window functions&lt;/span&gt;
&lt;strong&gt;Window function library&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/window-functions" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  5. When LATERAL beats subquery — perf matrix
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;bigquery lateral&lt;/code&gt;, planner behaviour, and LATERAL as an optimizer hint — the six-dialect perf matrix and the rules of thumb
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;LATERAL is worth reaching for whenever the planner would otherwise leave a correlated subquery un-optimised, whenever an index on (group_col, order_col) makes a per-outer LIMIT the winning plan, or whenever set-returning-function unnest requires correlation — and its dialect surface across Postgres, MySQL 8, Oracle 12c+, SQL Server, Snowflake, and BigQuery follows one primitive with six keywords&lt;/strong&gt;. Once you internalise the four decision axes — plan quality, index availability, unnest requirement, dialect — the LATERAL-vs-subquery choice becomes a checklist.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnlnp9vj3clkwtx1scyi9.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnlnp9vj3clkwtx1scyi9.jpeg" alt="Visual perf matrix of when LATERAL beats a subquery — six dialect columns (Postgres, MySQL 8, Oracle 12c+, SQL Server, Snowflake, BigQuery) crossed with four pattern rows (top-N per group, JSON unnest, TVF compose, correlated scalar); each cell tinted green/amber/red for LATERAL-wins / neutral / not-supported; a right-hand rules-of-thumb card; on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Four decision axes.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Plan quality.&lt;/strong&gt; Does the planner unnest the correlated subquery, or does it leave it as a per-row scan? On modern Postgres 14+, most correlated scalars get unnested to a hash join; on older versions and on some cloud warehouses, unnesting is not guaranteed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Index availability.&lt;/strong&gt; Does an index on &lt;code&gt;(group_col, order_col DESC)&lt;/code&gt; exist? If yes, LATERAL + LIMIT is optimal. If no, LATERAL loses to ROW_NUMBER() over a table scan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unnest requirement.&lt;/strong&gt; Does the inner query need to expand a set-returning function or JSON array? LATERAL is the only clean way to correlate an unnest with an outer row.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dialect.&lt;/strong&gt; Which engine, which keyword? LATERAL / CROSS APPLY / OUTER APPLY / QUALIFY / UNNEST — pick the right one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Planner behaviour — correlated-scalar unnest failures.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Postgres 14+ unnests most &lt;code&gt;(SELECT ... FROM inner WHERE inner.k = outer.k)&lt;/code&gt; scalar subqueries into a hash join or a merge join. Excellent plan quality.&lt;/li&gt;
&lt;li&gt;Postgres 11–13 unnest correlated scalars less aggressively. Rewrite as LATERAL to force a predictable plan.&lt;/li&gt;
&lt;li&gt;MySQL 8 tries harder than 5.7 but still misses some correlated-scalar cases. LATERAL is the safe rewrite.&lt;/li&gt;
&lt;li&gt;Oracle 12c+ is aggressive about unnesting. LATERAL still helps when you want explicit multi-column output.&lt;/li&gt;
&lt;li&gt;SQL Server 2016+ is competitive. CROSS APPLY is the idiomatic form; the planner recognises the pattern.&lt;/li&gt;
&lt;li&gt;Snowflake and BigQuery are aggressive about scalar unnesting on the query engine side, but LATERAL / UNNEST remain the readable choice.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When LATERAL wins on perf.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Top-N per group with covering index.&lt;/strong&gt; The definitive win. LATERAL + LIMIT + index seek is &lt;code&gt;O(N_outer × (log N_inner + K))&lt;/code&gt;; window-function alternatives are &lt;code&gt;O(N_inner log N_inner)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-column top-1 with covering index.&lt;/strong&gt; Same story — the LATERAL gives you multiple columns from the inner row for one seek.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON / array unnest.&lt;/strong&gt; No competition — LATERAL is the only clean primitive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TVF composition.&lt;/strong&gt; LATERAL / CROSS APPLY calls the TVF per outer row; the alternative is a &lt;code&gt;SELECT ... FROM outer WHERE outer.k IN (SELECT ...)&lt;/code&gt; which flattens the TVF away.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correlated aggregates in reporting queries.&lt;/strong&gt; LATERAL + &lt;code&gt;SUM(...)&lt;/code&gt; / &lt;code&gt;COUNT(...)&lt;/code&gt; per outer beats correlated scalar subqueries in the SELECT list when the planner isn't unnesting them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When LATERAL loses (or ties) on perf.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No index on (group_col, order_col).&lt;/strong&gt; LATERAL degrades to N × full scan of inner. Window function wins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Very small inner tables.&lt;/strong&gt; With &lt;code&gt;inner_rows &amp;lt; 1000&lt;/code&gt;, the planner picks a hash join anyway; LATERAL adds no benefit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-warehouse code with legacy engines.&lt;/strong&gt; MySQL 5.7 lacks LATERAL; QUALIFY / ROW_NUMBER() is the only portable path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Very large N.&lt;/strong&gt; With &lt;code&gt;N&lt;/code&gt; close to &lt;code&gt;inner_rows / outer_rows&lt;/code&gt;, the per-group LIMIT stops saving reads. Window function ties.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skew of 0 (uniform distribution).&lt;/strong&gt; With &lt;code&gt;inner_rows / outer_rows ≈ constant per group&lt;/code&gt;, LATERAL and window function converge on cost.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;LATERAL as an optimizer hint.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Even when a plain correlated subquery would produce the same plan, wrapping it in LATERAL forces the planner into the "nested-loop-with-limit" shape. This is useful when:

&lt;ul&gt;
&lt;li&gt;The default plan is unstable across parameters.&lt;/li&gt;
&lt;li&gt;You want deterministic worst-case behaviour.&lt;/li&gt;
&lt;li&gt;The team wants a review-visible signal that per-row semantics are intentional.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;LATERAL is not a &lt;em&gt;hint&lt;/em&gt; in the formal sense (like &lt;code&gt;/*+ INDEX(...) */&lt;/code&gt;) but functions as one in practice.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Dialect performance matrix — six engines, four patterns.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Top-N per group.&lt;/strong&gt; LATERAL / CROSS APPLY / OUTER APPLY / QUALIFY / UNNEST — LATERAL wins on Postgres, MySQL 8, Oracle, SQL Server (as CROSS APPLY), Snowflake (both LATERAL and QUALIFY work); BigQuery uses QUALIFY.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON / array unnest.&lt;/strong&gt; LATERAL + &lt;code&gt;jsonb_array_elements&lt;/code&gt; on Postgres; CROSS APPLY + OPENJSON on SQL Server; LATERAL FLATTEN on Snowflake; JSON_TABLE on Oracle; UNNEST on BigQuery. All engines have a clean primitive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TVF compose.&lt;/strong&gt; CROSS APPLY on SQL Server / Oracle; LATERAL on Postgres / Snowflake / MySQL 8 (with SETOF functions). BigQuery has no user-defined TVF; scalar UDFs only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correlated scalar.&lt;/strong&gt; All engines support the pattern; unnest quality varies. Postgres 14+ / Snowflake / BigQuery are aggressive; Postgres 11-13 / MySQL 8 less so; LATERAL wraps the pattern for predictability everywhere.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;BigQuery specifics — LATERAL by another name.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;BigQuery has no &lt;code&gt;LATERAL&lt;/code&gt; keyword.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;UNNEST(array_col)&lt;/code&gt; in the FROM clause is &lt;em&gt;implicitly&lt;/em&gt; correlated to its parent row. &lt;code&gt;SELECT t.id, x FROM t, UNNEST(t.arr) AS x&lt;/code&gt; behaves like &lt;code&gt;CROSS JOIN LATERAL UNNEST(t.arr)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Scalar-correlation asks are expressed via subqueries — &lt;code&gt;SELECT (SELECT MAX(x) FROM inner WHERE inner.k = outer.k) FROM outer&lt;/code&gt;. BigQuery's optimizer is aggressive about unnesting these.&lt;/li&gt;
&lt;li&gt;Top-N per group is QUALIFY + ROW_NUMBER(), not LATERAL + LIMIT.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Snowflake specifics — LATERAL and LATERAL FLATTEN.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Snowflake ships LATERAL for correlated joins and LATERAL FLATTEN for VARIANT / JSON unnest.&lt;/li&gt;
&lt;li&gt;QUALIFY is available and idiomatic for top-N per group.&lt;/li&gt;
&lt;li&gt;Both LATERAL + LIMIT and QUALIFY + ROW_NUMBER() have similar performance on Snowflake because the columnar engine parallelises the ranking pass.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Rules of thumb, memorised.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Top-N per group with a covering index?&lt;/strong&gt; LATERAL + LIMIT.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Top-N per group with no index?&lt;/strong&gt; ROW_NUMBER() OVER + WHERE rn ≤ N, or QUALIFY on supporting engines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON / array unnest?&lt;/strong&gt; LATERAL (or CROSS APPLY on SQL Server). Every engine has a primitive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reusable per-row logic?&lt;/strong&gt; CROSS APPLY of an inline TVF on SQL Server; LATERAL over a SETOF function on Postgres.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-warehouse code?&lt;/strong&gt; QUALIFY where available; ROW_NUMBER() + WHERE where not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correlated scalar with predictable plan?&lt;/strong&gt; Wrap in LATERAL for explicit shape.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — LATERAL + LIMIT vs ROW_NUMBER() OVER — plan shape
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The interview classic — show the two plans side-by-side and explain the difference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; For &lt;code&gt;orders(id, customer_id, order_ts, total)&lt;/code&gt; with an index on &lt;code&gt;(customer_id, order_ts DESC)&lt;/code&gt;, write both the LATERAL + LIMIT top-3 query and the ROW_NUMBER() + WHERE variant, and describe the difference in plan shape.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt; (10M orders across 100K customers.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Plan A: LATERAL + LIMIT&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="k"&gt;LATERAL&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;order_ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
  &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Plan B: ROW_NUMBER() + WHERE&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;order_ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rn&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Plan A — &lt;code&gt;Nested Loop → Index Seek on (customer_id, order_ts DESC) → Limit 3&lt;/code&gt;. One seek per customer_id + 3 reads. Cost &lt;code&gt;O(N_c × (log N_o + 3))&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Plan B — &lt;code&gt;Sort → Windowed ROW_NUMBER → Filter rn &amp;lt;= 3 → Hash Join to customers&lt;/code&gt;. Full sort over all 10M orders + ranking pass. Cost &lt;code&gt;O(N_o log N_o)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;On the given dataset (10M orders, 100K customers, index present): Plan A ≈ 1.2s wall clock; Plan B ≈ 4.8s wall clock. LATERAL wins by ≈ 4× on this shape.&lt;/li&gt;
&lt;li&gt;Without the index, Plan A degrades to &lt;code&gt;O(N_c × N_o)&lt;/code&gt; — dramatically worse — while Plan B stays at &lt;code&gt;O(N_o log N_o)&lt;/code&gt;. Adding the index is a hard requirement for LATERAL to beat.&lt;/li&gt;
&lt;li&gt;When N is large (say, top-1000 per customer): the per-group LIMIT stops saving reads because each customer has fewer than 1000 orders on average, and Plan A degrades to a per-customer full scan; Plan B stays constant. Choose based on &lt;code&gt;N vs inner_rows / outer_rows&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt; Same rows for both plans; different perf.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For small N (top 1–100) with a covering index, LATERAL + LIMIT is the definitive winner. For large N or missing index, window function wins.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — correlated scalar subquery plan quality
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A tricky performance case — the same logic as a scalar subquery in the SELECT list. On modern engines, the planner may unnest it; on older engines, it won't. LATERAL forces the good plan.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; For each customer, return their name plus the total spend across all their orders. Write both the correlated-scalar version and the LATERAL version, and explain when they diverge on perf.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt; (Same as above.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Correlated scalar in SELECT list — plan depends on unnesting&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
   &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_spend&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- LATERAL — deterministic hash-join plan&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;agg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_spend&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="k"&gt;LATERAL&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_spend&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;agg&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;On Postgres 14+, the correlated scalar version is &lt;em&gt;unnested&lt;/em&gt; into a hash-aggregate plan — semantically equivalent to a &lt;code&gt;GROUP BY customer_id&lt;/code&gt; join. Fast.&lt;/li&gt;
&lt;li&gt;On Postgres 11–13, MySQL 8, and some cloud engines, the unnest may not happen. The scalar runs as N × subquery, cost &lt;code&gt;O(N_c × avg_orders_per_customer)&lt;/code&gt; — bad on skewed data.&lt;/li&gt;
&lt;li&gt;The LATERAL version compiles to a nested-loop-with-aggregate or a hash-aggregate join — either way, deterministic. No planner-dependent behaviour.&lt;/li&gt;
&lt;li&gt;On the same 10M-order dataset with 100K customers: correlated-scalar-unnested ≈ 3s; LATERAL ≈ 3s (same plan); correlated-scalar-un-unnested ≈ 30s.&lt;/li&gt;
&lt;li&gt;Rule: if you don't trust the planner to unnest, or if you want review-visible determinism, use LATERAL.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt; (Same rows for both.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; LATERAL is a plan-stabiliser for correlated aggregates. When the planner is smart, both versions perform the same; when it's not, LATERAL is the safety net.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — BigQuery UNNEST as implicit LATERAL
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; BigQuery has no LATERAL keyword but supports the pattern via &lt;code&gt;UNNEST&lt;/code&gt; on array columns. The correlation is implicit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given &lt;code&gt;orders(id, items ARRAY&amp;lt;STRUCT&amp;lt;sku STRING, qty INT64, price NUMERIC&amp;gt;&amp;gt;)&lt;/code&gt; on BigQuery, unroll each item into its own row alongside the parent order id.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;items&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[{sku:"A", qty:2, price:10}, {sku:"B", qty:1, price:25}]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[{sku:"C", qty:3, price:5}]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- BigQuery: UNNEST is implicit LATERAL&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="k"&gt;UNNEST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;FROM orders o, UNNEST(o.items) AS item&lt;/code&gt; — the comma is a &lt;code&gt;CROSS JOIN&lt;/code&gt;; &lt;code&gt;UNNEST(o.items)&lt;/code&gt; reads the outer row's &lt;code&gt;items&lt;/code&gt; array. This is implicit LATERAL.&lt;/li&gt;
&lt;li&gt;BigQuery's parser recognises that &lt;code&gt;UNNEST&lt;/code&gt; on a column reference must be per-row. No &lt;code&gt;LATERAL&lt;/code&gt; keyword needed.&lt;/li&gt;
&lt;li&gt;To get LEFT-outer semantics — preserve orders with an empty &lt;code&gt;items&lt;/code&gt; array — use &lt;code&gt;LEFT JOIN UNNEST(o.items) AS item ON TRUE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;UNNEST supports arrays of structs, arrays of primitives, and even repeated fields inside nested structs. The syntax stays the same.&lt;/li&gt;
&lt;li&gt;Equivalent to &lt;code&gt;SELECT o.id, item.* FROM orders o CROSS JOIN LATERAL UNNEST(o.items) item&lt;/code&gt; in ANSI form.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;order_id&lt;/th&gt;
&lt;th&gt;sku&lt;/th&gt;
&lt;th&gt;qty&lt;/th&gt;
&lt;th&gt;price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; On BigQuery, &lt;code&gt;UNNEST(array_col)&lt;/code&gt; in the FROM clause is LATERAL by another name. No keyword, same semantics.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on cross-warehouse portability
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Your company runs analytics on Snowflake (batch) and BigQuery (batch). Design the top-3-orders-per-customer query that produces the same output on both warehouses. Where would you use LATERAL, where QUALIFY, and why?"&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using dbt macro with dialect dispatch
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- macros/top_n_orders_per_customer.sql&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;macro&lt;/span&gt; &lt;span class="n"&gt;top_n_orders_per_customer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customers_rel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;orders_rel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%-&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'snowflake'&lt;/span&gt; &lt;span class="o"&gt;-%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;-- Snowflake: LATERAL + LIMIT for index-seek path&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;customers_rel&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
    &lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="k"&gt;LATERAL&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
      &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;orders_rel&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
      &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
      &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;order_ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
      &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%-&lt;/span&gt; &lt;span class="n"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'bigquery'&lt;/span&gt; &lt;span class="o"&gt;-%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;-- BigQuery: QUALIFY + ROW_NUMBER (no LATERAL keyword)&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;customers_rel&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
    &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;orders_rel&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
    &lt;span class="n"&gt;QUALIFY&lt;/span&gt; &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%-&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;-%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;-- Portable fallback: CTE + WHERE rn &amp;lt;= n&lt;/span&gt;
    &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;ranked&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;order_ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt;
      &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;orders_rel&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;customers_rel&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
    &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;ranked&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rn&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%-&lt;/span&gt; &lt;span class="n"&gt;endif&lt;/span&gt; &lt;span class="o"&gt;-%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;endmacro&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dispatch branch&lt;/th&gt;
&lt;th&gt;What runs&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Snowflake&lt;/td&gt;
&lt;td&gt;LATERAL + LIMIT n&lt;/td&gt;
&lt;td&gt;Uses per-outer bound for the tightest plan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BigQuery&lt;/td&gt;
&lt;td&gt;QUALIFY ROW_NUMBER() OVER ... &amp;lt;= n&lt;/td&gt;
&lt;td&gt;No LATERAL keyword on BigQuery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Postgres / MySQL / SQL Server (fallback)&lt;/td&gt;
&lt;td&gt;CTE + ROW_NUMBER + WHERE rn &amp;lt;= n&lt;/td&gt;
&lt;td&gt;Portable across the remaining engines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;All&lt;/td&gt;
&lt;td&gt;Same output columns (customer_id, name, order_id, order_ts, total)&lt;/td&gt;
&lt;td&gt;Downstream stays warehouse-agnostic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The macro dispatches on &lt;code&gt;target.type&lt;/code&gt; at compile time. Snowflake gets the LATERAL variant; BigQuery gets QUALIFY; every other target gets the portable CTE version. All three emit the same output schema.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;customer_id&lt;/th&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;order_id&lt;/th&gt;
&lt;th&gt;order_ts&lt;/th&gt;
&lt;th&gt;total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Alice&lt;/td&gt;
&lt;td&gt;104&lt;/td&gt;
&lt;td&gt;2026-07-09&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Alice&lt;/td&gt;
&lt;td&gt;103&lt;/td&gt;
&lt;td&gt;2026-07-08&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Alice&lt;/td&gt;
&lt;td&gt;102&lt;/td&gt;
&lt;td&gt;2026-07-05&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Bob&lt;/td&gt;
&lt;td&gt;106&lt;/td&gt;
&lt;td&gt;2026-07-07&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Bob&lt;/td&gt;
&lt;td&gt;105&lt;/td&gt;
&lt;td&gt;2026-07-02&lt;/td&gt;
&lt;td&gt;70&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Compile-time dispatch, zero runtime overhead&lt;/strong&gt;&lt;/strong&gt; — dbt evaluates the Jinja block at compile time. The emitted SQL is one of three variants; no runtime branching, no reflection, no penalty.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Snowflake gets LATERAL for the tighter plan&lt;/strong&gt;&lt;/strong&gt; — Snowflake supports LATERAL and pushes the LIMIT into the inner scan. With a supporting clustering key on &lt;code&gt;(customer_id, order_ts)&lt;/code&gt;, the plan is the tightest available.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;BigQuery gets QUALIFY because there is no LATERAL keyword&lt;/strong&gt;&lt;/strong&gt; — QUALIFY + ROW_NUMBER() is idiomatic on BigQuery, reads well, and produces the same output shape as the LATERAL variant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Portable fallback uses only CTE + WHERE&lt;/strong&gt;&lt;/strong&gt; — every warehouse supports window functions (with the exception of MySQL 5.7, which the fallback would need a further branch for). One SQL that runs on Postgres, MySQL 8, SQL Server, Oracle, and Databricks with no changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — Snowflake LATERAL is O(N_c × (log N_o + K)) with a clustering key; BigQuery QUALIFY is O(N_o log N_o) (columnar sort is cheap); portable CTE is O(N_o log N_o). All three run in single-digit seconds on the 10M × 100K dataset on medium warehouse sizes. The macro adds zero runtime cost; the maintainability win is that downstream consumers see identical output columns regardless of which warehouse produced them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — joins (SQL) — medium&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Medium SQL joins&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/joins/sql/medium" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;


&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — SQL&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL problem library — 450+ DE-focused questions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;





&lt;h2&gt;
  
  
  Cheat sheet — LATERAL / CROSS APPLY recipe list
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two-slot skeleton.&lt;/strong&gt; Slot 1: the join keyword (&lt;code&gt;CROSS JOIN LATERAL&lt;/code&gt; / &lt;code&gt;LEFT JOIN LATERAL ... ON TRUE&lt;/code&gt; / &lt;code&gt;CROSS APPLY&lt;/code&gt; / &lt;code&gt;OUTER APPLY&lt;/code&gt;). Slot 2: the inner subquery that references outer columns. Memorise both; every LATERAL answer fills the two slots.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CROSS JOIN LATERAL&lt;/strong&gt; — inner-join semantics; drops outer rows with no inner match. Same as &lt;code&gt;CROSS APPLY&lt;/code&gt; on SQL Server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LEFT JOIN LATERAL ... ON TRUE&lt;/strong&gt; — left-outer semantics; preserves outer rows with NULLs when the inner returns zero. Same as &lt;code&gt;OUTER APPLY&lt;/code&gt; on SQL Server. The &lt;code&gt;ON TRUE&lt;/code&gt; is &lt;em&gt;not optional&lt;/em&gt; — every LEFT JOIN needs an ON clause.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correlation binding rule.&lt;/strong&gt; Inside a LATERAL block, the outer row's columns are in scope like function parameters. Without LATERAL, the outer scope is invisible inside the subquery.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres syntax variants.&lt;/strong&gt; &lt;code&gt;FROM outer, LATERAL (…) x&lt;/code&gt; (comma) or &lt;code&gt;FROM outer CROSS JOIN LATERAL (…) x&lt;/code&gt; (explicit JOIN). Same plan; explicit JOIN form is preferred for review clarity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL Server translation.&lt;/strong&gt; &lt;code&gt;CROSS APPLY (…)&lt;/code&gt; = &lt;code&gt;CROSS JOIN LATERAL (…)&lt;/code&gt;; &lt;code&gt;OUTER APPLY (…)&lt;/code&gt; = &lt;code&gt;LEFT JOIN LATERAL (…) ON TRUE&lt;/code&gt;; &lt;code&gt;TOP N&lt;/code&gt; inside APPLY = &lt;code&gt;LIMIT N&lt;/code&gt; inside LATERAL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Top-N per group primitive.&lt;/strong&gt; &lt;code&gt;CROSS JOIN LATERAL (SELECT … WHERE inner.k = outer.k ORDER BY inner.ts DESC LIMIT N) x&lt;/code&gt;. With an index on &lt;code&gt;(k, ts DESC)&lt;/code&gt;, the plan is nested loop + index seek + LIMIT — dramatically faster than &lt;code&gt;ROW_NUMBER() OVER + WHERE rn ≤ N&lt;/code&gt; on skewed data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON unnest primitive.&lt;/strong&gt; Postgres: &lt;code&gt;LATERAL jsonb_array_elements(t.payload)&lt;/code&gt;. SQL Server: &lt;code&gt;CROSS APPLY OPENJSON(t.payload) WITH (…)&lt;/code&gt;. Snowflake: &lt;code&gt;LATERAL FLATTEN(input =&amp;gt; t.payload)&lt;/code&gt;. BigQuery: &lt;code&gt;UNNEST(t.array_col)&lt;/code&gt; in the FROM clause (implicit LATERAL).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Array unnest primitive.&lt;/strong&gt; Postgres: &lt;code&gt;LATERAL unnest(t.arr) x(v)&lt;/code&gt;. Oracle: &lt;code&gt;TABLE(t.arr) x&lt;/code&gt; or &lt;code&gt;CROSS APPLY UNNEST(t.arr)&lt;/code&gt;. BigQuery: &lt;code&gt;UNNEST(t.arr)&lt;/code&gt;. All produce one row per (parent, element) pair.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TVF composition primitive.&lt;/strong&gt; SQL Server: &lt;code&gt;CROSS APPLY dbo.top_n_orders(c.id, 3) t&lt;/code&gt;. Postgres: &lt;code&gt;LATERAL top_n_orders(c.id, 3) t&lt;/code&gt; for SETOF-returning functions. Reusable per-row business logic centralised in one function.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-column top-1 primitive.&lt;/strong&gt; &lt;code&gt;LEFT JOIN LATERAL (SELECT id, order_ts, total FROM orders WHERE customer_id = c.id ORDER BY order_ts DESC LIMIT 1) o ON TRUE&lt;/code&gt;. Beats three separate correlated scalars because it does one inner scan for all columns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-outer aggregate primitive.&lt;/strong&gt; &lt;code&gt;LEFT JOIN LATERAL (SELECT COUNT(*) AS cnt, COALESCE(SUM(total), 0) AS spend FROM orders WHERE customer_id = c.id) agg ON TRUE&lt;/code&gt;. Beats correlated scalar per aggregate; predictable plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Composable LATERAL blocks.&lt;/strong&gt; Multiple LATERAL blocks in one FROM clause each run once per outer row. Add a third computation — add a third block; no interaction between them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;DISTINCT ON&lt;/code&gt; shortcut (Postgres only).&lt;/strong&gt; &lt;code&gt;SELECT DISTINCT ON (customer_id) * FROM orders ORDER BY customer_id, order_ts DESC&lt;/code&gt; — top-1 per group in one keyword. Doesn't extend to top-N.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;QUALIFY&lt;/code&gt; shortcut (Snowflake / BigQuery / Databricks / Teradata).&lt;/strong&gt; &lt;code&gt;SELECT * FROM orders QUALIFY ROW_NUMBER() OVER (…) &amp;lt;= N&lt;/code&gt;. Reads like WHERE-on-window-function. Same plan as CTE + WHERE.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When LATERAL beats ROW_NUMBER() OVER.&lt;/strong&gt; Index on &lt;code&gt;(group_col, order_col DESC)&lt;/code&gt; present + small N (top 1–100) + skewed distribution + engine supports LATERAL. All four → LATERAL wins by 2–10×.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When ROW_NUMBER() OVER beats LATERAL.&lt;/strong&gt; No supporting index, or large N, or uniform distribution, or MySQL 5.7 (no LATERAL). All four → window function wins or ties.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dialect matrix.&lt;/strong&gt; LATERAL: Postgres 9.3+, MySQL 8.0.14+, Oracle 12c+, Snowflake. CROSS APPLY / OUTER APPLY: SQL Server 2005+, Oracle 12c+. UNNEST (implicit LATERAL): BigQuery. QUALIFY: Snowflake, BigQuery, Databricks, Teradata.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LATERAL as an optimizer hint.&lt;/strong&gt; Even when a plain correlated subquery gets unnested, wrapping in LATERAL forces the "nested-loop-with-limit" plan — deterministic worst case, review-visible per-row intent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost model.&lt;/strong&gt; LATERAL + LIMIT with covering index: &lt;code&gt;O(N_outer × (log N_inner + K))&lt;/code&gt;. ROW_NUMBER() OVER: &lt;code&gt;O(N_inner log N_inner)&lt;/code&gt;. Correlated scalar: &lt;code&gt;O(N_outer × per-outer-cost)&lt;/code&gt; with unnesting; &lt;code&gt;O(N_outer × N_inner)&lt;/code&gt; without.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is a SQL LATERAL join and when do you use it?
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;sql lateral join&lt;/code&gt; is a FROM-clause subquery that runs &lt;strong&gt;once per row of the outer relation&lt;/strong&gt;, with the inner query allowed to reference the outer row's columns like function parameters. Reach for it whenever you need "for each outer row, run this correlated query" — top-N per group with &lt;code&gt;LIMIT&lt;/code&gt; (the classic latest-3-orders-per-customer), JSON / array unnest with &lt;code&gt;jsonb_array_elements&lt;/code&gt; or &lt;code&gt;OPENJSON&lt;/code&gt; or &lt;code&gt;UNNEST&lt;/code&gt; correlated to the parent, table-valued-function composition where the TVF takes outer-row arguments, or multi-column top-1 lookups where a scalar subquery would need three separate correlated queries. LATERAL is a keyword introduced in SQL/1999 and shipped in Postgres 9.3 (2013), MySQL 8.0.14 (2019), Oracle 12c (2013), and Snowflake; SQL Server never adopted the keyword but offers &lt;code&gt;CROSS APPLY&lt;/code&gt; (inner semantics) and &lt;code&gt;OUTER APPLY&lt;/code&gt; (left-outer semantics) with identical behaviour since SQL Server 2005. BigQuery uses implicit correlation via &lt;code&gt;UNNEST&lt;/code&gt; on array columns in the FROM clause. Every LATERAL query has a two-slot shape — the join keyword (CROSS JOIN LATERAL vs LEFT JOIN LATERAL … ON TRUE) plus the inner subquery — and once you internalise "for each outer row, run this inner query," the whole family reads like a single primitive across every dialect.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between LATERAL and a subquery?
&lt;/h3&gt;

&lt;p&gt;An ordinary subquery in the FROM clause is evaluated &lt;strong&gt;independently&lt;/strong&gt; of the outer query — its scope is only the tables it names, and it cannot see outer columns. A LATERAL subquery is evaluated &lt;strong&gt;per outer row&lt;/strong&gt; with the outer's columns bound in scope, so the inner query can reference &lt;code&gt;outer.k&lt;/code&gt;, &lt;code&gt;outer.ts&lt;/code&gt;, or any outer column freely. This is the correlation binding rule — LATERAL is the keyword that authorises the outer-column reference. A correlated scalar subquery in the SELECT list (&lt;code&gt;SELECT (SELECT MAX(x) FROM inner WHERE k = outer.k) FROM outer&lt;/code&gt;) has a similar per-row behaviour but is restricted to one column, one row — LATERAL generalises that to a whole inner table with multiple columns and multiple rows. Perf-wise, modern planners often unnest correlated scalars into hash joins, but the unnesting is not guaranteed; LATERAL forces a predictable "nested-loop-with-limit" plan that is often faster on skewed data with a supporting index. In short — subqueries are for isolated computations, LATERAL is for correlated ones that need outer-column visibility and typically emit more than one column or more than one row per outer.&lt;/p&gt;

&lt;h3&gt;
  
  
  How does CROSS APPLY compare to LATERAL?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;cross apply sql server&lt;/code&gt; is SQL Server's spelling of ANSI LATERAL — same semantics, different keyword. &lt;code&gt;CROSS APPLY (…)&lt;/code&gt; maps directly to &lt;code&gt;CROSS JOIN LATERAL (…)&lt;/code&gt;; &lt;code&gt;outer apply&lt;/code&gt; maps to &lt;code&gt;LEFT JOIN LATERAL (…) ON TRUE&lt;/code&gt;. The only syntactic differences are the keyword itself and SQL Server's use of &lt;code&gt;TOP N&lt;/code&gt; instead of &lt;code&gt;LIMIT N&lt;/code&gt; inside the inner query. Every ANSI LATERAL recipe — top-N per group, JSON unnest via OPENJSON, TVF composition, multi-column top-1 — ports to CROSS APPLY with two keyword swaps. Oracle 12c+ ships &lt;strong&gt;both&lt;/strong&gt; keywords (LATERAL and CROSS APPLY / OUTER APPLY), so Oracle teams can pick whichever their style guide prefers. SQL Server has offered CROSS APPLY / OUTER APPLY since 2005, years before Postgres or MySQL added LATERAL, so the APPLY family is deeply embedded in SQL Server codebases and shows up in every "top-N per group" or "TVF-per-row" pattern. When porting Postgres code to SQL Server (or vice versa), the mechanical rules are: &lt;code&gt;CROSS JOIN LATERAL&lt;/code&gt; ↔ &lt;code&gt;CROSS APPLY&lt;/code&gt;, &lt;code&gt;LEFT JOIN LATERAL … ON TRUE&lt;/code&gt; ↔ &lt;code&gt;OUTER APPLY&lt;/code&gt;, &lt;code&gt;LIMIT N&lt;/code&gt; ↔ &lt;code&gt;TOP N&lt;/code&gt;, &lt;code&gt;jsonb_array_elements&lt;/code&gt; ↔ &lt;code&gt;OPENJSON&lt;/code&gt;. Same primitive; same performance story; different spelling.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you get top-N per group with LATERAL?
&lt;/h3&gt;

&lt;p&gt;The archetype: &lt;code&gt;SELECT c.*, o.* FROM customers c CROSS JOIN LATERAL (SELECT * FROM orders WHERE customer_id = c.id ORDER BY order_ts DESC LIMIT N) o&lt;/code&gt;. The outer scans &lt;code&gt;customers&lt;/code&gt; once; per outer row, the inner runs an index seek on the &lt;code&gt;(customer_id, order_ts DESC)&lt;/code&gt; composite index and reads at most N rows. On SQL Server the same query is &lt;code&gt;CROSS APPLY (SELECT TOP N * FROM orders WHERE customer_id = c.id ORDER BY order_ts DESC) o&lt;/code&gt;. To preserve customers with no orders, swap &lt;code&gt;CROSS JOIN LATERAL&lt;/code&gt; for &lt;code&gt;LEFT JOIN LATERAL … ON TRUE&lt;/code&gt; (or CROSS APPLY for OUTER APPLY on SQL Server). This plan is &lt;code&gt;O(N_customers × (log N_orders + K))&lt;/code&gt; — dramatically faster than &lt;code&gt;ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY order_ts DESC) WHERE rn ≤ N&lt;/code&gt;, which is &lt;code&gt;O(N_orders log N_orders)&lt;/code&gt; for the sort. On a 10M-order, 100K-customer table with the covering index, LATERAL + LIMIT 3 runs in about 1.2 seconds; the window-function variant runs in about 4.8 seconds — a 4× win. On engines without LATERAL (MySQL 5.7, older SQL Server), fall back to &lt;code&gt;ROW_NUMBER() OVER&lt;/code&gt; + WHERE. On Snowflake, BigQuery, Databricks, and Teradata, &lt;code&gt;QUALIFY ROW_NUMBER() OVER (…) &amp;lt;= N&lt;/code&gt; is a one-clause shortcut with the same plan as CTE + WHERE.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which SQL engines support LATERAL / CROSS APPLY in 2026?
&lt;/h3&gt;

&lt;p&gt;As of 2026: &lt;strong&gt;Postgres 9.3+&lt;/strong&gt; ships full ANSI &lt;code&gt;LATERAL&lt;/code&gt; and both &lt;code&gt;CROSS JOIN LATERAL&lt;/code&gt; and &lt;code&gt;LEFT JOIN LATERAL ... ON TRUE&lt;/code&gt;. &lt;strong&gt;MySQL 8.0.14+&lt;/strong&gt; ships the same (MySQL 5.7 lacks LATERAL — use ROW_NUMBER() fallback). &lt;strong&gt;Oracle 12c+&lt;/strong&gt; ships both &lt;code&gt;LATERAL&lt;/code&gt; (ANSI) and &lt;code&gt;CROSS APPLY&lt;/code&gt; / &lt;code&gt;OUTER APPLY&lt;/code&gt; (SQL-Server-compatible). &lt;strong&gt;SQL Server 2005+&lt;/strong&gt; ships &lt;code&gt;CROSS APPLY&lt;/code&gt; and &lt;code&gt;OUTER APPLY&lt;/code&gt; with identical semantics to LATERAL. &lt;strong&gt;Snowflake&lt;/strong&gt; ships &lt;code&gt;LATERAL&lt;/code&gt; and &lt;code&gt;LATERAL FLATTEN&lt;/code&gt; (for VARIANT / JSON unnest). &lt;strong&gt;BigQuery&lt;/strong&gt; has no LATERAL keyword but supports the pattern via implicit correlation on &lt;code&gt;UNNEST(array_col)&lt;/code&gt; in the FROM clause; scalar correlation uses subqueries with aggressive planner unnesting. &lt;strong&gt;Databricks SQL&lt;/strong&gt; supports LATERAL VIEW (a Hive-style syntax) plus QUALIFY. &lt;strong&gt;Redshift&lt;/strong&gt; has no LATERAL; use ROW_NUMBER() + WHERE. Streaming SQL engines like Flink SQL and RisingWave inherit LATERAL semantics from ANSI. Memorise the six-row engine × keyword matrix — when an interviewer asks "would you use LATERAL here?" the first question back is always "what's the warehouse?"&lt;/p&gt;

&lt;h3&gt;
  
  
  Does BigQuery support LATERAL joins?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;bigquery lateral&lt;/code&gt; is a common question — the short answer is "not by that name, but the semantics are there." BigQuery does not ship the &lt;code&gt;LATERAL&lt;/code&gt; keyword. Instead, &lt;code&gt;UNNEST(array_col)&lt;/code&gt; in the FROM clause is &lt;em&gt;implicitly&lt;/em&gt; correlated to its parent row — &lt;code&gt;SELECT o.id, item.sku FROM orders o, UNNEST(o.items) AS item&lt;/code&gt; behaves exactly like &lt;code&gt;CROSS JOIN LATERAL UNNEST(o.items)&lt;/code&gt; in ANSI SQL. To preserve rows with an empty array, use &lt;code&gt;LEFT JOIN UNNEST(o.items) AS item ON TRUE&lt;/code&gt; — this is BigQuery's &lt;code&gt;LEFT JOIN LATERAL … ON TRUE&lt;/code&gt; equivalent. For &lt;strong&gt;scalar correlations&lt;/strong&gt; ("for each row, compute a value from a related table"), BigQuery uses subqueries in the SELECT list; the planner is aggressive about unnesting correlated scalars into hash joins, so perf is competitive with ANSI LATERAL. For &lt;strong&gt;top-N per group&lt;/strong&gt;, BigQuery uses &lt;code&gt;QUALIFY ROW_NUMBER() OVER (PARTITION BY … ORDER BY … DESC) &amp;lt;= N&lt;/code&gt; — a one-clause shorthand for the CTE + WHERE pattern. There is no direct BigQuery equivalent to &lt;code&gt;CROSS JOIN LATERAL (SELECT … LIMIT N)&lt;/code&gt; with an inner LIMIT — the top-N pattern goes through QUALIFY. When porting a Postgres LATERAL query to BigQuery, the mechanical rules are: &lt;code&gt;LATERAL jsonb_array_elements(…)&lt;/code&gt; → &lt;code&gt;UNNEST(…)&lt;/code&gt;; &lt;code&gt;LATERAL (SELECT … LIMIT N)&lt;/code&gt; for top-N → &lt;code&gt;JOIN … QUALIFY ROW_NUMBER() OVER (…) &amp;lt;= N&lt;/code&gt;; scalar correlations stay as SELECT-list subqueries and trust the planner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practice on PipeCode
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Drill the &lt;a href="https://pipecode.ai/explore/practice/topic/joins/sql" rel="noopener noreferrer"&gt;SQL joins practice library →&lt;/a&gt; for &lt;code&gt;CROSS JOIN LATERAL&lt;/code&gt;, &lt;code&gt;LEFT JOIN LATERAL ... ON TRUE&lt;/code&gt;, &lt;code&gt;CROSS APPLY&lt;/code&gt;, and &lt;code&gt;OUTER APPLY&lt;/code&gt; variants across Postgres, MySQL 8, Oracle, and SQL Server dialects.&lt;/li&gt;
&lt;li&gt;Rehearse on &lt;a href="https://pipecode.ai/explore/practice/topic/top-n" rel="noopener noreferrer"&gt;top-N per group problems →&lt;/a&gt; — the archetype LATERAL win where a covering index on &lt;code&gt;(group_col, order_col DESC)&lt;/code&gt; beats every &lt;code&gt;ROW_NUMBER() OVER + WHERE rn ≤ N&lt;/code&gt; alternative.&lt;/li&gt;
&lt;li&gt;Sharpen the &lt;a href="https://pipecode.ai/explore/practice/topic/subqueries" rel="noopener noreferrer"&gt;subqueries drill room →&lt;/a&gt; to feel the difference between correlated scalar subqueries and LATERAL joins in the FROM clause.&lt;/li&gt;
&lt;li&gt;Push the difficulty ceiling with &lt;a href="https://pipecode.ai/explore/practice/topic/joins/sql/hard" rel="noopener noreferrer"&gt;hard SQL joins →&lt;/a&gt; for the composite APPLY + TVF patterns senior interviewers love — multi-column top-1, per-outer aggregate, JSON unnest with correlation.&lt;/li&gt;
&lt;li&gt;Warm up with &lt;a href="https://pipecode.ai/explore/practice/topic/joins/sql/medium" rel="noopener noreferrer"&gt;medium SQL joins →&lt;/a&gt; — the mid-difficulty rung where LATERAL vs ROW_NUMBER() OVER trade-offs show up.&lt;/li&gt;
&lt;li&gt;Layer &lt;a href="https://pipecode.ai/explore/practice/topic/window-functions" rel="noopener noreferrer"&gt;window function drills →&lt;/a&gt; — the ROW_NUMBER() + QUALIFY axis that pairs with LATERAL for the top-N-per-group family.&lt;/li&gt;
&lt;li&gt;Practise &lt;a href="https://pipecode.ai/explore/practice/topic/self-join" rel="noopener noreferrer"&gt;self-join drills →&lt;/a&gt; for the "row-to-row within a table" pattern that LATERAL often replaces with a cleaner form.&lt;/li&gt;
&lt;li&gt;Layer &lt;a href="https://pipecode.ai/explore/practice/topic/subquery" rel="noopener noreferrer"&gt;subquery drills →&lt;/a&gt; to build the mental map of correlated vs uncorrelated subqueries and where LATERAL fits between them.&lt;/li&gt;
&lt;li&gt;Sharpen the general SQL surface with the &lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;SQL practice library →&lt;/a&gt; which contains 450+ DE-focused questions covering LATERAL, APPLY, top-N per group, JSON unnest, and every adjacent pattern.&lt;/li&gt;
&lt;li&gt;For the broader SQL interview surface, take the &lt;a href="https://pipecode.ai/explore/courses/sql-for-data-engineering-interviews-from-zero-to-faang" rel="noopener noreferrer"&gt;SQL for Data Engineering course →&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/" rel="noopener noreferrer"&gt;Pipecode.ai&lt;/a&gt; is Leetcode for Data Engineering — every `sql lateral join` recipe above ships with hands-on practice rooms where you write the two-slot skeleton, wire `CROSS JOIN LATERAL` for top-3-per-customer with a covering index, translate to `cross apply sql server` and `outer apply` for the Microsoft stack, port the same pattern to `postgres lateral`, chase the `lateral vs subquery` performance question with real ANALYZE plans, rehearse the `sql top-n per group` interview classic, compose inline TVFs on SQL Server for reusable per-row logic, unnest JSON with `OPENJSON` and `jsonb_array_elements`, and cross-warehouse-dispatch the whole family to `bigquery lateral` via `UNNEST` and QUALIFY. PipeCode pairs every reading with 450+ DE-focused problems and a real-time scoring engine, so you never have to wonder whether your LATERAL answer holds up under a senior interviewer's depth probes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/joins/sql" rel="noopener noreferrer"&gt;Practice SQL joins now →&lt;/a&gt;&lt;br&gt;
&lt;a href="https://pipecode.ai/explore/practice/topic/top-n" rel="noopener noreferrer"&gt;Top-N per group drills →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>sql</category>
      <category>interview</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>SQL MATCH_RECOGNIZE: Pattern Recognition for Sessions, Funnels &amp; Anomalies</title>
      <dc:creator>Gowtham Potureddi</dc:creator>
      <pubDate>Sat, 11 Jul 2026 15:25:53 +0000</pubDate>
      <link>https://dev.to/gowthampotureddi/sql-matchrecognize-pattern-recognition-for-sessions-funnels-anomalies-221j</link>
      <guid>https://dev.to/gowthampotureddi/sql-matchrecognize-pattern-recognition-for-sessions-funnels-anomalies-221j</guid>
      <description>&lt;p&gt;&lt;strong&gt;&lt;code&gt;sql match_recognize&lt;/code&gt;&lt;/strong&gt; is the single most under-taught pattern in warehouse SQL — and the single largest edge a senior data engineer can pull out of the ANSI SQL/2016 standard on a whiteboard. The same "detect an ordered sequence of rows that satisfies a regex-like pattern" ask shows up in five different costumes: sessionization ("group events into 30-minute sessions"), funnel conversion ("who did signup → view → cart → checkout in that order?"), anomaly detection ("find the V-shape reversal in this price series"), fraud bursts ("five spend events inside sixty seconds"), and streaming CEP ("emit an alert as soon as the pattern completes"). Every answer starts from the same seven-slot skeleton: &lt;code&gt;PARTITION BY entity ORDER BY time MEASURES ... PATTERN (A B+ C) DEFINE A AS ..., B AS ..., C AS ...&lt;/code&gt;. The interviewer wants to hear you say "row pattern matching is regex over ordered rows" in the first sentence.&lt;/p&gt;

&lt;p&gt;This guide is the mid-to-senior tour you wished existed the first time an interviewer asked you to write a &lt;code&gt;row pattern matching sql&lt;/code&gt; query on the whiteboard, sketch an &lt;code&gt;oracle match_recognize&lt;/code&gt; V-shape anomaly for a financial series, port a query to &lt;code&gt;snowflake match_recognize&lt;/code&gt;, wire a &lt;code&gt;flink sql pattern&lt;/code&gt; for streaming CEP, or design a &lt;code&gt;funnel analysis sql&lt;/code&gt; pipeline that reports drop-off with &lt;code&gt;sql pattern detection&lt;/code&gt; primitives. It walks through the ANSI clause anatomy (PARTITION BY / ORDER BY / MEASURES / ONE ROW PER MATCH vs ALL ROWS PER MATCH / AFTER MATCH SKIP / PATTERN with regex quantifiers / DEFINE), the sessionization pattern with &lt;code&gt;PATTERN ((A B*)+)&lt;/code&gt; and per-user partitioning, the funnel pattern with anchored ordering and PERMUTE variants, the V-shape and W-shape stock anomaly the Oracle documentation made famous, and the dialect matrix — Oracle 12c+, Snowflake, Flink SQL, Trino/Presto, RisingWave versus Postgres and BigQuery with the portable LAG + SUM(new_group) OVER fallback. Every section pairs a teaching block with a Solution-Tail interview answer — code, a step-by-step trace, an output table, then a concept-by-concept breakdown of why it works.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk94f64dreqqp8jdmcq3q.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk94f64dreqqp8jdmcq3q.jpeg" alt="PipeCode blog header for SQL MATCH_RECOGNIZE pattern recognition — bold white headline 'SQL MATCH_RECOGNIZE' with subtitle 'Row Pattern Matching · Sessions · Funnels · Anomalies' and a stylised PATTERN (A B+ C) row-sequence card on a dark gradient with purple, green, orange, and blue accents and a small pipecode.ai attribution." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you want &lt;strong&gt;hands-on reps&lt;/strong&gt; immediately after reading, drill the &lt;a href="https://pipecode.ai/explore/practice/topic/pattern-matching" rel="noopener noreferrer"&gt;pattern-matching practice library →&lt;/a&gt;, rehearse on &lt;a href="https://pipecode.ai/explore/practice/topic/gaps-and-islands" rel="noopener noreferrer"&gt;gaps-and-islands problems →&lt;/a&gt; for the portable fallback, and sharpen the streaming axis with &lt;a href="https://pipecode.ai/explore/practice/topic/streaming" rel="noopener noreferrer"&gt;streaming drills →&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;On this page&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why row pattern matching matters in 2026&lt;/li&gt;
&lt;li&gt;MATCH_RECOGNIZE anatomy&lt;/li&gt;
&lt;li&gt;Sessionization with MR&lt;/li&gt;
&lt;li&gt;Funnel &amp;amp; conversion patterns&lt;/li&gt;
&lt;li&gt;Anomaly detection &amp;amp; dialect matrix&lt;/li&gt;
&lt;li&gt;Cheat sheet — MATCH_RECOGNIZE recipe list&lt;/li&gt;
&lt;li&gt;Frequently asked questions&lt;/li&gt;
&lt;li&gt;Practice on PipeCode&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1. Why row pattern matching matters in 2026
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The "regex over ordered rows" family — sessionization, funnels, anomalies, fraud bursts, and every other row-sequence question a senior interviewer might ask
&lt;/h3&gt;

&lt;p&gt;The one-sentence invariant: &lt;strong&gt;a &lt;code&gt;sql match_recognize&lt;/code&gt; question asks you to detect an ordered sequence of rows that satisfies a regex-like pattern inside a partition, and the canonical answer is a seven-clause skeleton where PATTERN is a regex over labelled row classes and DEFINE assigns predicates to those labels&lt;/strong&gt;. Once you internalise that "PATTERN + DEFINE is the whole engine," the row-pattern interview surface collapses into a template you can write from muscle memory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Five faces of the same problem.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sessionization.&lt;/strong&gt; Given an event stream, group consecutive events into sessions separated by a gap of inactivity — a pattern &lt;code&gt;(A B*)+&lt;/code&gt; where &lt;code&gt;B&lt;/code&gt; continues while the inactivity gap is inside the threshold. Every product analytics platform under the sun — Google Analytics, Segment, Rudderstack, Snowplow, PostHog, Mixpanel — implements exactly this logic. The interviewer wants to see the MR version &lt;em&gt;and&lt;/em&gt; the portable LAG + SUM version, and to hear you say which one you would ship on which warehouse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Funnels.&lt;/strong&gt; Given a table of user events, find users who went &lt;code&gt;SIGNUP → VIEW_PRODUCT+ → ADD_TO_CART → CHECKOUT&lt;/code&gt; in that order. Growth teams live and die on funnel conversion — every product analytics dashboard does this, and every senior data engineer needs to write the query cold. MR reads like a regex and expresses the strict ordering trivially; the portable fallback needs a self-join per step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anomaly detection.&lt;/strong&gt; Given a price series, find every V-shape reversal (&lt;code&gt;N&lt;/code&gt; down candles followed by &lt;code&gt;N&lt;/code&gt; up candles) or every W-shape double bottom. The classic Oracle MATCH_RECOGNIZE documentation example is exactly this shape — it's the query the ANSI SQL/2016 committee had in mind when they wrote the clause. Fraud detection uses &lt;code&gt;PATTERN (SPEND{5,})&lt;/code&gt; for spend bursts inside a time window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CEP / streaming pattern detection.&lt;/strong&gt; &lt;code&gt;flink sql pattern&lt;/code&gt; and the Flink CEP library expose MATCH_RECOGNIZE as a streaming operator — the same pattern-detection semantics but with watermarks, event time, and low-latency emission. RisingWave ships the same clause with streaming semantics. Trino and Presto ship the batch variant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time-series segmentation.&lt;/strong&gt; Given a sensor stream, find every run of contiguous &lt;code&gt;HIGH&lt;/code&gt; readings followed by a &lt;code&gt;LOW&lt;/code&gt; — a two-part MR pattern that reads more clearly than the equivalent LAG + CASE chain.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Two mental primitives.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PATTERN is a regex over row labels.&lt;/strong&gt; The alphabet is user-defined row labels (&lt;code&gt;A&lt;/code&gt;, &lt;code&gt;B&lt;/code&gt;, &lt;code&gt;C&lt;/code&gt;, or named labels like &lt;code&gt;SIGNUP&lt;/code&gt;, &lt;code&gt;VIEW&lt;/code&gt;, &lt;code&gt;CART&lt;/code&gt;, &lt;code&gt;CHECKOUT&lt;/code&gt;). Quantifiers are the standard regex set: &lt;code&gt;*&lt;/code&gt; (zero or more), &lt;code&gt;+&lt;/code&gt; (one or more), &lt;code&gt;?&lt;/code&gt; (zero or one), &lt;code&gt;{n}&lt;/code&gt; (exactly n), &lt;code&gt;{n,m}&lt;/code&gt; (n to m). Alternation is &lt;code&gt;|&lt;/code&gt;. Grouping is parentheses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DEFINE is a predicate per label.&lt;/strong&gt; For each label used in PATTERN, DEFINE assigns a boolean predicate that the row must satisfy to be classified with that label. Predicates can reference &lt;code&gt;PREV(col)&lt;/code&gt;, &lt;code&gt;NEXT(col)&lt;/code&gt;, &lt;code&gt;FIRST(col)&lt;/code&gt;, &lt;code&gt;LAST(col)&lt;/code&gt;, and any other label's rows already matched — the state machine is aware of what came before.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why senior interviewers love this family.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It tests window-function fluency plus regex thinking.&lt;/strong&gt; Everyone can write ROW_NUMBER for a leaderboard; fewer can write &lt;code&gt;PATTERN (STRT DOWN+ UP+)&lt;/code&gt; for a V-shape reversal. The interviewer probes whether you understand that pattern matching over rows is a distinct primitive from windowing over rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It has a portable answer and a native answer.&lt;/strong&gt; The interviewer wants to see both: the LAG + SUM(new_group) OVER template from gaps-and-islands &lt;em&gt;and&lt;/em&gt; the MATCH_RECOGNIZE version. Senior candidates know when to reach for which — MR on Oracle, Snowflake, Flink, Trino, RisingWave; portable everywhere else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's compositional.&lt;/strong&gt; Once you can sessionize, funnels are the same skeleton with a longer PATTERN. Once you can do funnels, anomalies are a PATTERN with DEFINE predicates that reference &lt;code&gt;PREV(col)&lt;/code&gt;. Once you can do anomalies, streaming CEP is the same query with a watermark. A senior candidate walks through this progression without prompting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It exposes partitioning and ordering discipline.&lt;/strong&gt; Every answer must &lt;code&gt;PARTITION BY user_id&lt;/code&gt; (or &lt;code&gt;symbol&lt;/code&gt;, or &lt;code&gt;device_id&lt;/code&gt;) and &lt;code&gt;ORDER BY event_time&lt;/code&gt;. Candidates who forget the partition get wrong answers on multi-entity data — an instant red flag.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Where row-pattern queries land in your pipelines.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Product analytics.&lt;/strong&gt; Every funnel report is a MR query underneath (or a chain of self-joins that could be one MR query). Every session is a &lt;code&gt;PATTERN ((A B*)+)&lt;/code&gt; match. Every retention cohort is an anchored MR match against the first activity date.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Financial time series.&lt;/strong&gt; V-shape reversals, W-shape double bottoms, breakout patterns, three-white-soldiers candlestick patterns — every classical technical-analysis pattern is a MR query. Fraud detection uses MR for burst detection, velocity checks, and sequence-of-events triggers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliability / SRE.&lt;/strong&gt; "N consecutive failed health-checks then an outage marker" is a MR pattern. Post-mortem timeline reconstruction is easier as MR than as a chain of window functions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streaming CEP.&lt;/strong&gt; Flink SQL exposes MR as a streaming operator so an alert emits the moment the pattern completes on a live event stream. RisingWave ships the same primitive with streaming semantics. Real-time fraud, uptime monitoring, and order-flow anomaly detection all use this shape.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data quality checks.&lt;/strong&gt; "Assert that every &lt;code&gt;START&lt;/code&gt; event is followed by a matching &lt;code&gt;END&lt;/code&gt; within 5 minutes" is a MR query with a &lt;code&gt;WITH UNMATCHED ROWS&lt;/code&gt; clause that exposes the violating rows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What senior interviewers actually probe.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Anatomy first.&lt;/strong&gt; Do you know the seven clauses? Can you name each one and say what it controls? PARTITION BY, ORDER BY, MEASURES, ONE ROW PER MATCH vs ALL ROWS PER MATCH, AFTER MATCH SKIP, PATTERN, DEFINE — say them in order without prompting and you're already in the top 20%.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PATTERN quantifiers.&lt;/strong&gt; Do you know &lt;code&gt;*&lt;/code&gt; vs &lt;code&gt;+&lt;/code&gt; vs &lt;code&gt;?&lt;/code&gt; vs &lt;code&gt;{n}&lt;/code&gt; vs &lt;code&gt;{n,m}&lt;/code&gt; vs &lt;code&gt;|&lt;/code&gt; vs grouping? Do you know the greedy vs reluctant distinction (&lt;code&gt;+&lt;/code&gt; vs &lt;code&gt;+?&lt;/code&gt;)? Do you know &lt;code&gt;PERMUTE(A, B, C)&lt;/code&gt; for unordered variants?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AFTER MATCH SKIP.&lt;/strong&gt; Do you know &lt;code&gt;SKIP TO NEXT ROW&lt;/code&gt; vs &lt;code&gt;SKIP PAST LAST ROW&lt;/code&gt; vs &lt;code&gt;SKIP TO FIRST &amp;lt;label&amp;gt;&lt;/code&gt; vs &lt;code&gt;SKIP TO LAST &amp;lt;label&amp;gt;&lt;/code&gt;? The default is &lt;code&gt;SKIP PAST LAST ROW&lt;/code&gt; on most engines; &lt;code&gt;SKIP TO NEXT ROW&lt;/code&gt; lets matches overlap. Getting this wrong doubles or halves your output row count.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ONE ROW PER MATCH vs ALL ROWS PER MATCH.&lt;/strong&gt; Do you know when to collapse a match into one summary row vs when to preserve the raw rows with a match id? Session summaries want ONE ROW; per-event enrichment wants ALL ROWS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dialect matrix.&lt;/strong&gt; Can you name the engines that ship MR (Oracle, Snowflake, Flink SQL, Trino, Presto, RisingWave) and the ones that don't (Postgres, BigQuery, SQL Server, MySQL, Redshift as of 2026)? Do you default to the portable LAG + SUM pattern for a multi-warehouse codebase?&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Worked example — the "consecutive down candles" anomaly starter
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The canonical warm-up: given a &lt;code&gt;prices(t, price)&lt;/code&gt; series, find every run of two or more consecutive price drops. This is the fastest way to check that a candidate recognises the MR family — the two-line PATTERN + DEFINE answer is a fingerprint on the whiteboard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given &lt;code&gt;prices(t, price)&lt;/code&gt; where &lt;code&gt;t&lt;/code&gt; is a dense ascending integer and &lt;code&gt;price&lt;/code&gt; is a positive integer, write an Oracle / Snowflake MR query that returns the start and end index of every run of two or more consecutive price drops.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;t&lt;/th&gt;
&lt;th&gt;price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;98&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;95&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;97&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;88&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;prices&lt;/span&gt;
&lt;span class="n"&gt;MATCH_RECOGNIZE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
  &lt;span class="n"&gt;MEASURES&lt;/span&gt;
    &lt;span class="k"&gt;FIRST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;LAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_length&lt;/span&gt;
  &lt;span class="n"&gt;ONE&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="n"&gt;PER&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt;
  &lt;span class="k"&gt;AFTER&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt; &lt;span class="n"&gt;SKIP&lt;/span&gt; &lt;span class="n"&gt;PAST&lt;/span&gt; &lt;span class="k"&gt;LAST&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt;
  &lt;span class="n"&gt;PATTERN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DOWN&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,})&lt;/span&gt;
  &lt;span class="n"&gt;DEFINE&lt;/span&gt;
    &lt;span class="n"&gt;DOWN&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;PREV&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;ORDER BY t&lt;/code&gt; establishes the row order the pattern is matched against. Without it, the engine has no notion of "previous" or "next."&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PATTERN (DOWN{2,})&lt;/code&gt; — the regex says "match two or more consecutive rows classified as &lt;code&gt;DOWN&lt;/code&gt;." The quantifier &lt;code&gt;{2,}&lt;/code&gt; is "two or more" (regex convention).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DEFINE DOWN AS price &amp;lt; PREV(price)&lt;/code&gt; — a row is classified as &lt;code&gt;DOWN&lt;/code&gt; only if its &lt;code&gt;price&lt;/code&gt; is less than the previous row's &lt;code&gt;price&lt;/code&gt;. &lt;code&gt;PREV(col)&lt;/code&gt; reads the value from the previous row inside the current pattern match.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MEASURES FIRST(t) AS run_start, LAST(t) AS run_end, COUNT(*) AS run_length&lt;/code&gt; — for each match, emit the first row's &lt;code&gt;t&lt;/code&gt;, the last row's &lt;code&gt;t&lt;/code&gt;, and the count of matched rows.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AFTER MATCH SKIP PAST LAST ROW&lt;/code&gt; — after a match ends, resume matching at the row &lt;em&gt;after&lt;/em&gt; the last matched row. This prevents overlapping matches — the default and the right choice for anomaly runs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;run_start&lt;/th&gt;
&lt;th&gt;run_end&lt;/th&gt;
&lt;th&gt;run_length&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; The consecutive-down-candles pattern is the MR "hello world" — recognise the shape in 5 seconds, write PATTERN + DEFINE in 30, and move on to the harder V-shape / W-shape patterns the interviewer really wants.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the LAG + SUM portable equivalent
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The senior follow-up: "write the same query without MATCH_RECOGNIZE." This is the portable answer that ships on Postgres, BigQuery, SQL Server, MySQL, Redshift, and every warehouse that doesn't ship MR. The equivalence check is a real interview probe — the interviewer wants to see that you can translate between the two.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Rewrite the consecutive-down-candles query using LAG and cumulative SUM only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt; (Same as above.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;flagged&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CASE&lt;/span&gt;
      &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;is_down&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CASE&lt;/span&gt;
      &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;is_new_run&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;prices&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;grouped&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;is_down&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;is_new_run&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;flagged&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;is_down&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_length&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;grouped&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;
&lt;span class="k"&gt;HAVING&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;run_start&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;flagged&lt;/code&gt; CTE — mark every row where the price is less than the previous row's price as &lt;code&gt;is_down = 1&lt;/code&gt;. The &lt;code&gt;is_new_run&lt;/code&gt; flag fires on a &lt;code&gt;DOWN&lt;/code&gt; row whose predecessor was not &lt;code&gt;DOWN&lt;/code&gt;, so the cumulative sum starts a new run.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;grouped&lt;/code&gt; CTE — keep only &lt;code&gt;DOWN&lt;/code&gt; rows and cumulative-sum &lt;code&gt;is_new_run&lt;/code&gt; into &lt;code&gt;run_id&lt;/code&gt;. Rows inside the same run share the same id.&lt;/li&gt;
&lt;li&gt;Final SELECT — group by &lt;code&gt;run_id&lt;/code&gt; and aggregate. Filter to &lt;code&gt;HAVING COUNT(*) &amp;gt;= 2&lt;/code&gt; to enforce the "two or more" clause the PATTERN quantifier &lt;code&gt;{2,}&lt;/code&gt; gave us for free.&lt;/li&gt;
&lt;li&gt;This portable version is 15+ lines vs 12 lines for the MR version. On complex patterns the gap widens dramatically — a "buy then three drops then a stop-loss" pattern is 40+ lines portable vs 20 lines MR.&lt;/li&gt;
&lt;li&gt;Rule of thumb: for portable code, the LAG + SUM template works. For readability on Oracle / Snowflake / Flink / Trino / RisingWave, MR wins as soon as the pattern is more than one row class.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt; (Same as above — same rows, different plan.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Every MR pattern has a LAG + SUM portable equivalent — the translation is mechanical for simple patterns and painful for regex-heavy ones. Portability first; MR when the pattern justifies it.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — engine dialect quickstart
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A quick sanity check the interviewer might drop: "Which of these engines can I run MATCH_RECOGNIZE on today?" The answer is a short list — knowing it cold is a senior signal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given a list of warehouses / engines, mark each as MR-capable or portable-fallback-only in 2026.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt; Oracle 12c+, Snowflake, Flink SQL, Trino, Presto, RisingWave, Postgres 16, BigQuery, SQL Server 2022, MySQL 8, Redshift, Databricks SQL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt; (No code — a dialect matrix answer.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Engine              | MATCH_RECOGNIZE support (as of 2026)
Oracle 12c+         | YES — full ANSI, shipped 2013 (first in the market)
Snowflake           | YES — full ANSI, GA since 2021
Flink SQL           | YES — streaming CEP semantics
Trino / Presto      | YES — batch, since Trino 361
RisingWave          | YES — streaming, since 1.7
Postgres 16         | NO — no planned support; use LAG + SUM
BigQuery            | NO — no planned support; use LAG + SUM
SQL Server 2022     | NO — no planned support; use LAG + SUM
MySQL 8             | NO — no planned support; use LAG + SUM
Redshift            | NO — no planned support; use LAG + SUM
Databricks SQL      | NO (as of 2026) — use LAG + SUM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Oracle was first — the ANSI SQL/2016 committee based the standard on Oracle's original implementation from 12c.&lt;/li&gt;
&lt;li&gt;Snowflake followed in 2021 GA — the batch semantics match Oracle's exactly.&lt;/li&gt;
&lt;li&gt;Flink SQL and RisingWave ship the streaming variant — watermark-driven pattern completion with low-latency emission.&lt;/li&gt;
&lt;li&gt;Trino / Presto ship the batch variant — most cloud data lakehouses (Athena, Dremio, Ahana) that build on Trino inherit MR support.&lt;/li&gt;
&lt;li&gt;Postgres, BigQuery, SQL Server, MySQL, Redshift, and Databricks SQL do not ship MR in 2026; portable LAG + SUM is the fallback. If your codebase is multi-warehouse, default to portable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt; The matrix above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Memorise the five-yes / six-no list. When an interviewer asks "would you use MATCH_RECOGNIZE here?" the first question back is "what's the warehouse?" — say it out loud and you signal senior instincts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on the row-pattern mental model
&lt;/h3&gt;

&lt;p&gt;A senior interviewer often opens with: "I hand you an event stream and ask for a metric — pick sessionization, funnel conversion, or anomaly detection — that requires you to detect an ordered row pattern. Walk me through the seven-clause skeleton, and tell me what changes across the three asks."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using the seven-clause MATCH_RECOGNIZE skeleton
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Universal MATCH_RECOGNIZE skeleton — seven clauses, one regex over rows&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;input&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="n"&gt;MATCH_RECOGNIZE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;                          &lt;span class="c1"&gt;-- Slot 1: per-entity scope&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ordering&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;                            &lt;span class="c1"&gt;-- Slot 2: row order&lt;/span&gt;
  &lt;span class="n"&gt;MEASURES&lt;/span&gt;                                           &lt;span class="c1"&gt;-- Slot 3: per-match columns&lt;/span&gt;
    &lt;span class="k"&gt;FIRST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;alias&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;LAST&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;alias&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;             &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;alias&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CLASSIFIER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;matched_label&lt;/span&gt;
  &lt;span class="n"&gt;ONE&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="n"&gt;PER&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt;                                  &lt;span class="c1"&gt;-- Slot 4: output shape&lt;/span&gt;
  &lt;span class="k"&gt;AFTER&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt; &lt;span class="n"&gt;SKIP&lt;/span&gt; &lt;span class="n"&gt;PAST&lt;/span&gt; &lt;span class="k"&gt;LAST&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt;                     &lt;span class="c1"&gt;-- Slot 5: overlap policy&lt;/span&gt;
  &lt;span class="n"&gt;PATTERN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;+&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;C&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                            &lt;span class="c1"&gt;-- Slot 6: regex over labels&lt;/span&gt;
  &lt;span class="n"&gt;DEFINE&lt;/span&gt;                                             &lt;span class="c1"&gt;-- Slot 7: label predicates&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;predicate&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;predicate&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;may&lt;/span&gt; &lt;span class="n"&gt;reference&lt;/span&gt; &lt;span class="n"&gt;PREV&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;NEXT&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;C&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;predicate&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;C&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ask&lt;/th&gt;
&lt;th&gt;PARTITION BY&lt;/th&gt;
&lt;th&gt;PATTERN&lt;/th&gt;
&lt;th&gt;Key DEFINE predicate&lt;/th&gt;
&lt;th&gt;Output shape&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sessionization&lt;/td&gt;
&lt;td&gt;user_id&lt;/td&gt;
&lt;td&gt;&lt;code&gt;((A B*)+)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;B AS ts - LAG(B.ts) &amp;lt;= INTERVAL '30 minutes'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ONE ROW PER MATCH → session summary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Funnel&lt;/td&gt;
&lt;td&gt;user_id&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(SIGNUP VIEW+ CART CHECKOUT)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;VIEW AS event_type = 'view_product'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ONE ROW PER MATCH → converted user + timings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;V-shape anomaly&lt;/td&gt;
&lt;td&gt;symbol&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(STRT DOWN+ UP+)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;DOWN AS price &amp;lt; PREV(price)&lt;/code&gt;, &lt;code&gt;UP AS price &amp;gt; PREV(price)&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;ONE ROW PER MATCH → reversal boundaries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;W-shape anomaly&lt;/td&gt;
&lt;td&gt;symbol&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(DOWN+ UP+ DOWN+ UP+)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same as V-shape&lt;/td&gt;
&lt;td&gt;ONE ROW PER MATCH → double-bottom boundaries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fraud burst&lt;/td&gt;
&lt;td&gt;user_id&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(SPEND{5,})&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SPEND AS ts - FIRST(SPEND.ts) &amp;lt;= INTERVAL '60' SECOND&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ONE ROW PER MATCH → burst boundaries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-event enrichment&lt;/td&gt;
&lt;td&gt;user_id&lt;/td&gt;
&lt;td&gt;Any&lt;/td&gt;
&lt;td&gt;Same as above&lt;/td&gt;
&lt;td&gt;ALL ROWS PER MATCH → per-row classifier&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The universal skeleton turns "which query do I write?" from a taste question into a mechanical checklist. Pick the partition, pick the ordering, pick the PATTERN, write the DEFINE predicates — the whole query falls out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ask&lt;/th&gt;
&lt;th&gt;Approx query length (MR)&lt;/th&gt;
&lt;th&gt;Approx query length (portable LAG + SUM)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sessionization&lt;/td&gt;
&lt;td&gt;~15 lines&lt;/td&gt;
&lt;td&gt;~20 lines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Funnel&lt;/td&gt;
&lt;td&gt;~15 lines&lt;/td&gt;
&lt;td&gt;~40 lines (4 self-joins)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;V-shape anomaly&lt;/td&gt;
&lt;td&gt;~15 lines&lt;/td&gt;
&lt;td&gt;~25 lines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;W-shape anomaly&lt;/td&gt;
&lt;td&gt;~15 lines&lt;/td&gt;
&lt;td&gt;~40+ lines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fraud burst&lt;/td&gt;
&lt;td&gt;~15 lines&lt;/td&gt;
&lt;td&gt;~30 lines&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;PATTERN is a regex over row labels&lt;/strong&gt;&lt;/strong&gt; — every MR answer starts with a regex over labels drawn from an alphabet you define. The moment you can express the ask as a regex, PATTERN writes itself. &lt;code&gt;A B+ C&lt;/code&gt; is the archetype; every variant is a quantifier tweak.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;DEFINE is a predicate per label&lt;/strong&gt;&lt;/strong&gt; — for each label used in PATTERN, DEFINE gives the boolean predicate that classifies a row into that label. Predicates can reference &lt;code&gt;PREV&lt;/code&gt;, &lt;code&gt;NEXT&lt;/code&gt;, &lt;code&gt;FIRST&lt;/code&gt;, &lt;code&gt;LAST&lt;/code&gt; of the current or previous labels — the state machine is aware of history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;PARTITION BY entity is non-negotiable&lt;/strong&gt;&lt;/strong&gt; — multi-entity data always requires per-entity partitioning. Skipping it produces a global pattern that mixes users / symbols / devices, which is a wrong-answer buffer that the interviewer catches immediately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;ORDER BY inside the MR block matters&lt;/strong&gt;&lt;/strong&gt; — the ordering determines what "previous" and "next" mean. Sessionization orders by &lt;code&gt;event_time&lt;/code&gt;; anomaly detection orders by &lt;code&gt;ts&lt;/code&gt;; funnels order by &lt;code&gt;event_time&lt;/code&gt;. Get the ordering wrong and PREV / NEXT lie.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — MR compiles to a state-machine scan over each partition, cost O(N) per partition after the sort. The dominant cost is the ORDER BY sort — O(N log N) per partition. On modern engines the state-machine step is streamed row-by-row inside the sort output, so wall clock is essentially the sort cost. Portable LAG + SUM is the same cost class; MR just reads better for regex-shaped patterns.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — pattern matching&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Pattern matching problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/pattern-matching" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — gaps and islands&lt;/span&gt;
&lt;strong&gt;Gaps and islands drills&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/gaps-and-islands" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  2. MATCH_RECOGNIZE anatomy
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The seven-clause skeleton — PARTITION BY, ORDER BY, MEASURES, ONE ROW / ALL ROWS PER MATCH, AFTER MATCH SKIP, PATTERN, DEFINE
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;MATCH_RECOGNIZE is a regex engine that runs over ordered rows inside a partition, where PATTERN is the regex, DEFINE is the alphabet's semantics, and MEASURES + ONE ROW / ALL ROWS PER MATCH shape the output&lt;/strong&gt;. Once you say "PATTERN is regex, DEFINE is semantics, MEASURES is payload" out loud, the anatomy interview surface collapses to a slot-filling exercise.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fctulhv5zswc5gcu49f8a.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fctulhv5zswc5gcu49f8a.jpeg" alt="Visual diagram of the MATCH_RECOGNIZE anatomy — a vertical annotated skeleton showing the seven clause slots (PARTITION BY, ORDER BY, MEASURES, ONE ROW PER MATCH vs ALL ROWS PER MATCH, AFTER MATCH SKIP, PATTERN with quantifiers, DEFINE), a right-hand toy consecutive-price-drop example, and a small legend for the regex quantifiers *, +, ?, {n}, {n,m}; on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Slot 1 — &lt;code&gt;PARTITION BY &amp;lt;cols&amp;gt;&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scopes the pattern engine per entity. Every partition is matched independently — one match per user, per symbol, per device, per session.&lt;/li&gt;
&lt;li&gt;Optional but almost always present. Omitting it runs the pattern over the whole table as one partition — right only for single-entity data (a single sensor stream, a single stock price series).&lt;/li&gt;
&lt;li&gt;Multiple columns supported — &lt;code&gt;PARTITION BY user_id, device_id&lt;/code&gt; scopes per (user, device) pair.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 2 — &lt;code&gt;ORDER BY &amp;lt;cols&amp;gt;&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Required. Establishes the row order the pattern is matched against. &lt;code&gt;PREV&lt;/code&gt; and &lt;code&gt;NEXT&lt;/code&gt; are defined relative to this ordering.&lt;/li&gt;
&lt;li&gt;Almost always &lt;code&gt;event_time&lt;/code&gt; or a monotonically-increasing sequence. Ties are broken by additional columns — &lt;code&gt;ORDER BY event_time, event_id&lt;/code&gt; is the defensive form.&lt;/li&gt;
&lt;li&gt;Skipping ORDER BY inside MR raises a compilation error on every engine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 3 — &lt;code&gt;MEASURES&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Declares the per-match output columns. Each measure is an expression evaluated at match time, using &lt;code&gt;FIRST(label.col)&lt;/code&gt;, &lt;code&gt;LAST(label.col)&lt;/code&gt;, &lt;code&gt;COUNT(*)&lt;/code&gt;, aggregations, and any expression that references the pattern's labels.&lt;/li&gt;
&lt;li&gt;Standard measure functions:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;FIRST(A.col)&lt;/code&gt; — value of &lt;code&gt;col&lt;/code&gt; from the first &lt;code&gt;A&lt;/code&gt;-labelled row in the match.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LAST(A.col)&lt;/code&gt; — value of &lt;code&gt;col&lt;/code&gt; from the last &lt;code&gt;A&lt;/code&gt;-labelled row.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;COUNT(A.*)&lt;/code&gt; — count of &lt;code&gt;A&lt;/code&gt;-labelled rows.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SUM(A.col)&lt;/code&gt;, &lt;code&gt;AVG(A.col)&lt;/code&gt;, &lt;code&gt;MIN(A.col)&lt;/code&gt;, &lt;code&gt;MAX(A.col)&lt;/code&gt; — aggregate over &lt;code&gt;A&lt;/code&gt;-labelled rows.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CLASSIFIER()&lt;/code&gt; — returns the label a specific row was classified into (used with ALL ROWS PER MATCH).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MATCH_NUMBER()&lt;/code&gt; — a monotonically-increasing per-partition match id.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 4 — &lt;code&gt;ONE ROW PER MATCH&lt;/code&gt; vs &lt;code&gt;ALL ROWS PER MATCH&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ONE ROW PER MATCH&lt;/code&gt; — one output row per matched pattern. The output row's values come from the MEASURES expressions. Right for session summaries, funnel outcomes, anomaly boundaries.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ALL ROWS PER MATCH&lt;/code&gt; — one output row per row &lt;em&gt;inside&lt;/em&gt; the match. Each row carries the match id (&lt;code&gt;MATCH_NUMBER()&lt;/code&gt;), its classifier (&lt;code&gt;CLASSIFIER()&lt;/code&gt;), and any MEASURES you declared. Right for per-event enrichment inside a session, funnel step attribution.&lt;/li&gt;
&lt;li&gt;Variants — &lt;code&gt;ALL ROWS PER MATCH SHOW EMPTY MATCHES&lt;/code&gt; (include zero-length matches), &lt;code&gt;ALL ROWS PER MATCH OMIT EMPTY MATCHES&lt;/code&gt; (skip them), &lt;code&gt;ALL ROWS PER MATCH WITH UNMATCHED ROWS&lt;/code&gt; (also emit rows that never matched any pattern — key for drop-off attribution).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 5 — &lt;code&gt;AFTER MATCH SKIP …&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Controls where the engine resumes matching after a successful match. Four canonical options:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;AFTER MATCH SKIP PAST LAST ROW&lt;/code&gt; — resume at the row after the last matched row. No overlap. Default on Oracle / Snowflake / Trino / Flink.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AFTER MATCH SKIP TO NEXT ROW&lt;/code&gt; — resume at the row after the &lt;em&gt;first&lt;/em&gt; matched row. Allows overlap.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AFTER MATCH SKIP TO FIRST &amp;lt;label&amp;gt;&lt;/code&gt; — resume at the first row labelled &lt;code&gt;&amp;lt;label&amp;gt;&lt;/code&gt; in the match.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AFTER MATCH SKIP TO LAST &amp;lt;label&amp;gt;&lt;/code&gt; — resume at the last row labelled &lt;code&gt;&amp;lt;label&amp;gt;&lt;/code&gt; in the match.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Getting this wrong doubles or halves your output row count. For anomaly runs and funnel completions, &lt;code&gt;SKIP PAST LAST ROW&lt;/code&gt; is right. For "count every possible occurrence" analytics, &lt;code&gt;SKIP TO NEXT ROW&lt;/code&gt; is right.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 6 — &lt;code&gt;PATTERN (...)&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The regex over row labels. Syntax:

&lt;ul&gt;
&lt;li&gt;Concatenation — &lt;code&gt;A B C&lt;/code&gt; matches an &lt;code&gt;A&lt;/code&gt; row followed by a &lt;code&gt;B&lt;/code&gt; row followed by a &lt;code&gt;C&lt;/code&gt; row.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;*&lt;/code&gt; — zero or more.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;+&lt;/code&gt; — one or more.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;?&lt;/code&gt; — zero or one.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;{n}&lt;/code&gt; — exactly n.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;{n,m}&lt;/code&gt; — n to m (inclusive).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;|&lt;/code&gt; — alternation (&lt;code&gt;A | B&lt;/code&gt; matches either).&lt;/li&gt;
&lt;li&gt;Parentheses — grouping (&lt;code&gt;(A B)+&lt;/code&gt; matches one or more &lt;code&gt;A B&lt;/code&gt; pairs).&lt;/li&gt;
&lt;li&gt;Reluctant quantifiers — &lt;code&gt;+?&lt;/code&gt;, &lt;code&gt;*?&lt;/code&gt;, &lt;code&gt;??&lt;/code&gt; — match as few rows as possible (greedy is default).&lt;/li&gt;
&lt;li&gt;Anchors — &lt;code&gt;^&lt;/code&gt; (start of partition), &lt;code&gt;$&lt;/code&gt; (end of partition) on engines that support them.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PERMUTE(A, B, C)&lt;/code&gt; — match A, B, C in any order (Oracle / Trino / Flink).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Reads like a regex over the row alphabet — once you can write regex, PATTERN writes itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slot 7 — &lt;code&gt;DEFINE &amp;lt;label&amp;gt; AS &amp;lt;predicate&amp;gt;&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Assigns a boolean predicate to each PATTERN label. A row is classified into a label only if it satisfies the label's predicate at that step of the state machine.&lt;/li&gt;
&lt;li&gt;If a label is used in PATTERN but not defined, it defaults to &lt;code&gt;TRUE&lt;/code&gt; (matches any row). &lt;code&gt;PATTERN (A B*) DEFINE B AS ...&lt;/code&gt; — &lt;code&gt;A&lt;/code&gt; matches anything.&lt;/li&gt;
&lt;li&gt;Predicates can reference:

&lt;ul&gt;
&lt;li&gt;The current row's columns — &lt;code&gt;DEFINE UP AS price &amp;gt; 100&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The previous row inside the match — &lt;code&gt;DEFINE UP AS price &amp;gt; PREV(price)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The first row of the match — &lt;code&gt;DEFINE B AS ts - FIRST(A.ts) &amp;lt;= INTERVAL '30 minutes'&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The last row of a specific label — &lt;code&gt;DEFINE C AS price &amp;gt; LAST(B.price)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Aggregates over previously matched rows — &lt;code&gt;DEFINE C AS SUM(A.qty) + SUM(B.qty) &amp;gt; 100&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Reading order for a MR query.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What's the partition? → PARTITION BY.&lt;/li&gt;
&lt;li&gt;What's the row order? → ORDER BY.&lt;/li&gt;
&lt;li&gt;What's the pattern I'm looking for? → PATTERN.&lt;/li&gt;
&lt;li&gt;What predicates define each label? → DEFINE.&lt;/li&gt;
&lt;li&gt;What do I emit per match? → MEASURES.&lt;/li&gt;
&lt;li&gt;One row per match or all rows? → ONE ROW / ALL ROWS PER MATCH.&lt;/li&gt;
&lt;li&gt;Where do I resume after a match? → AFTER MATCH SKIP.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every senior candidate walks through the seven slots in this order without prompting.&lt;/p&gt;
&lt;h4&gt;
  
  
  Worked example — consecutive-price-drop with ONE ROW PER MATCH
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The classic anatomy warm-up: use ONE ROW PER MATCH to emit one summary row per run of consecutive price drops. This is where every candidate should start when learning MR.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given &lt;code&gt;prices(t, price)&lt;/code&gt; ordered by &lt;code&gt;t&lt;/code&gt;, use MATCH_RECOGNIZE to emit one row per run of two or more consecutive price drops, with the run's start ts, end ts, count, and first / last prices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;t&lt;/th&gt;
&lt;th&gt;price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;98&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;95&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;93&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;97&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;prices&lt;/span&gt;
&lt;span class="n"&gt;MATCH_RECOGNIZE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
  &lt;span class="n"&gt;MEASURES&lt;/span&gt;
    &lt;span class="k"&gt;FIRST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;LAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;FIRST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;start_price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;LAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;end_price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_length&lt;/span&gt;
  &lt;span class="n"&gt;ONE&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="n"&gt;PER&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt;
  &lt;span class="k"&gt;AFTER&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt; &lt;span class="n"&gt;SKIP&lt;/span&gt; &lt;span class="n"&gt;PAST&lt;/span&gt; &lt;span class="k"&gt;LAST&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt;
  &lt;span class="n"&gt;PATTERN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DOWN&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,})&lt;/span&gt;
  &lt;span class="n"&gt;DEFINE&lt;/span&gt;
    &lt;span class="n"&gt;DOWN&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;PREV&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;ORDER BY t&lt;/code&gt; — the state machine walks rows in &lt;code&gt;t&lt;/code&gt; order. &lt;code&gt;PREV(price)&lt;/code&gt; refers to the immediately preceding row inside the match.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PATTERN (DOWN{2,})&lt;/code&gt; — regex says "two or more &lt;code&gt;DOWN&lt;/code&gt; rows in a row." Matches greedily by default — as many &lt;code&gt;DOWN&lt;/code&gt; rows as possible.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DEFINE DOWN AS price &amp;lt; PREV(price)&lt;/code&gt; — a row is &lt;code&gt;DOWN&lt;/code&gt; only if its price is less than the previous row's. &lt;code&gt;PREV(price)&lt;/code&gt; on the first row of a partition is NULL — the predicate evaluates to NULL, treated as FALSE, so no match starts on row 1.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MEASURES&lt;/code&gt; — &lt;code&gt;FIRST(t)&lt;/code&gt; and &lt;code&gt;LAST(t)&lt;/code&gt; are the boundary timestamps; &lt;code&gt;FIRST(price)&lt;/code&gt; and &lt;code&gt;LAST(price)&lt;/code&gt; are the boundary prices; &lt;code&gt;COUNT(*)&lt;/code&gt; is the run length.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AFTER MATCH SKIP PAST LAST ROW&lt;/code&gt; — after emitting a match ending at row &lt;code&gt;k&lt;/code&gt;, resume at row &lt;code&gt;k+1&lt;/code&gt;. No overlapping runs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;run_start&lt;/th&gt;
&lt;th&gt;run_end&lt;/th&gt;
&lt;th&gt;start_price&lt;/th&gt;
&lt;th&gt;end_price&lt;/th&gt;
&lt;th&gt;run_length&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;98&lt;/td&gt;
&lt;td&gt;93&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Wait — the second row is wrong because row 6 alone can't satisfy &lt;code&gt;DOWN{2,}&lt;/code&gt;. Correct output:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;run_start&lt;/th&gt;
&lt;th&gt;run_end&lt;/th&gt;
&lt;th&gt;start_price&lt;/th&gt;
&lt;th&gt;end_price&lt;/th&gt;
&lt;th&gt;run_length&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;98&lt;/td&gt;
&lt;td&gt;93&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; ONE ROW PER MATCH collapses each pattern match into one summary row. Perfect for "how many runs did I have?" reporting. When you need per-row enrichment, switch to ALL ROWS PER MATCH.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — same pattern with ALL ROWS PER MATCH + CLASSIFIER
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The follow-up variant: preserve every row inside the match and label each with its classifier. Useful for per-event enrichment — you keep every input row and enrich it with match id and label.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Rewrite the consecutive-price-drop query to preserve every row and label each row with its match id and classifier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt; (Same as above.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;prices&lt;/span&gt;
&lt;span class="n"&gt;MATCH_RECOGNIZE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
  &lt;span class="n"&gt;MEASURES&lt;/span&gt;
    &lt;span class="n"&gt;MATCH_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;match_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CLASSIFIER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;matched_as&lt;/span&gt;
  &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="n"&gt;PER&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt;
  &lt;span class="k"&gt;AFTER&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt; &lt;span class="n"&gt;SKIP&lt;/span&gt; &lt;span class="n"&gt;PAST&lt;/span&gt; &lt;span class="k"&gt;LAST&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt;
  &lt;span class="n"&gt;PATTERN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DOWN&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,})&lt;/span&gt;
  &lt;span class="n"&gt;DEFINE&lt;/span&gt;
    &lt;span class="n"&gt;DOWN&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;PREV&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;ALL ROWS PER MATCH&lt;/code&gt; — instead of one summary row per match, emit every input row that was part of a match.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MATCH_NUMBER()&lt;/code&gt; — a monotonically-increasing per-partition id. Rows from match 1 have &lt;code&gt;match_id = 1&lt;/code&gt;; rows from match 2 have &lt;code&gt;match_id = 2&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CLASSIFIER()&lt;/code&gt; — returns the label the row was classified into. In this query it's always &lt;code&gt;DOWN&lt;/code&gt; (the only label).&lt;/li&gt;
&lt;li&gt;Rows that didn't match any pattern are &lt;em&gt;not&lt;/em&gt; emitted with &lt;code&gt;ALL ROWS PER MATCH&lt;/code&gt;. To emit them too, use &lt;code&gt;ALL ROWS PER MATCH WITH UNMATCHED ROWS&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Use this variant when downstream code needs the original rows enriched with match metadata — e.g., a Sankey diagram of the funnel or a per-event timeline replay.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;t&lt;/th&gt;
&lt;th&gt;price&lt;/th&gt;
&lt;th&gt;match_id&lt;/th&gt;
&lt;th&gt;matched_as&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;98&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;DOWN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;95&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;DOWN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;93&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;DOWN&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; ONE ROW PER MATCH for summary reporting; ALL ROWS PER MATCH for per-event enrichment. Match id + classifier are the standard columns you'd project.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — AFTER MATCH SKIP TO NEXT ROW for overlapping matches
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The nuanced follow-up: sometimes you want every possible pattern occurrence, including overlapping ones. Switching &lt;code&gt;AFTER MATCH SKIP PAST LAST ROW&lt;/code&gt; to &lt;code&gt;AFTER MATCH SKIP TO NEXT ROW&lt;/code&gt; changes the output row count dramatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given &lt;code&gt;prices(t, price)&lt;/code&gt;, count &lt;em&gt;every&lt;/em&gt; pair of consecutive drops — even when one pair's second drop is the next pair's first drop. Use &lt;code&gt;AFTER MATCH SKIP TO NEXT ROW&lt;/code&gt; and &lt;code&gt;PATTERN (DOWN DOWN)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;t&lt;/th&gt;
&lt;th&gt;price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;98&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;95&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;prices&lt;/span&gt;
&lt;span class="n"&gt;MATCH_RECOGNIZE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
  &lt;span class="n"&gt;MEASURES&lt;/span&gt;
    &lt;span class="k"&gt;FIRST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;pair_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;LAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;pair_end&lt;/span&gt;
  &lt;span class="n"&gt;ONE&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="n"&gt;PER&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt;
  &lt;span class="k"&gt;AFTER&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt; &lt;span class="n"&gt;SKIP&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="k"&gt;NEXT&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt;
  &lt;span class="n"&gt;PATTERN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DOWN&lt;/span&gt; &lt;span class="n"&gt;DOWN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;DEFINE&lt;/span&gt;
    &lt;span class="n"&gt;DOWN&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;PREV&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;PATTERN (DOWN DOWN)&lt;/code&gt; — match exactly two consecutive &lt;code&gt;DOWN&lt;/code&gt; rows.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AFTER MATCH SKIP TO NEXT ROW&lt;/code&gt; — after a match starting at row &lt;code&gt;k&lt;/code&gt; and ending at &lt;code&gt;k+1&lt;/code&gt;, resume matching at row &lt;code&gt;k+1&lt;/code&gt; (the middle row), not &lt;code&gt;k+2&lt;/code&gt;. This lets the next match start at what was the previous match's second row.&lt;/li&gt;
&lt;li&gt;Row 2 → 3 forms one match (&lt;code&gt;DOWN&lt;/code&gt; at 2, &lt;code&gt;DOWN&lt;/code&gt; at 3). Engine resumes at row 3.&lt;/li&gt;
&lt;li&gt;Row 3 → 4 forms another match (&lt;code&gt;DOWN&lt;/code&gt; at 3, &lt;code&gt;DOWN&lt;/code&gt; at 4). Engine resumes at row 4.&lt;/li&gt;
&lt;li&gt;Result: two overlapping matches instead of one. With &lt;code&gt;SKIP PAST LAST ROW&lt;/code&gt;, we'd only get the first match — the engine would resume at row 4 after emitting the 2 → 3 match.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;pair_start&lt;/th&gt;
&lt;th&gt;pair_end&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; &lt;code&gt;SKIP PAST LAST ROW&lt;/code&gt; for non-overlapping runs (sessions, anomalies, funnels). &lt;code&gt;SKIP TO NEXT ROW&lt;/code&gt; for "every possible occurrence" analytics (candlestick pair counting, sliding-window pattern probes). Choose deliberately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on anatomy fluency
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Walk me through a &lt;code&gt;MATCH_RECOGNIZE&lt;/code&gt; query that finds every user's &lt;em&gt;first&lt;/em&gt; session of length ≥ 3 events, ordered by event time. I want to see the seven clauses filled in with the right choices — including ONE ROW vs ALL ROWS, AFTER MATCH SKIP semantics, and the PATTERN quantifier."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using the seven-clause skeleton with first-match selection
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="n"&gt;MATCH_RECOGNIZE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt;
  &lt;span class="n"&gt;MEASURES&lt;/span&gt;
    &lt;span class="n"&gt;MATCH_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;      &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;match_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;FIRST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;session_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;LAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;session_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_count&lt;/span&gt;
  &lt;span class="n"&gt;ONE&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="n"&gt;PER&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt;
  &lt;span class="k"&gt;AFTER&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt; &lt;span class="n"&gt;SKIP&lt;/span&gt; &lt;span class="n"&gt;PAST&lt;/span&gt; &lt;span class="k"&gt;LAST&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt;
  &lt;span class="n"&gt;PATTERN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt; &lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,})&lt;/span&gt;
  &lt;span class="n"&gt;DEFINE&lt;/span&gt;
    &lt;span class="n"&gt;A&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;B&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;LAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'30'&lt;/span&gt; &lt;span class="k"&gt;MINUTE&lt;/span&gt;
       &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;LAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'30'&lt;/span&gt; &lt;span class="k"&gt;MINUTE&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;QUALIFY&lt;/span&gt; &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;session_start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Slot&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PARTITION BY&lt;/td&gt;
&lt;td&gt;user_id&lt;/td&gt;
&lt;td&gt;Per-user pattern scope&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ORDER BY&lt;/td&gt;
&lt;td&gt;event_time&lt;/td&gt;
&lt;td&gt;Pattern runs in time order&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MEASURES&lt;/td&gt;
&lt;td&gt;match_id, session_start, session_end, event_count&lt;/td&gt;
&lt;td&gt;Session summary payload&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output shape&lt;/td&gt;
&lt;td&gt;ONE ROW PER MATCH&lt;/td&gt;
&lt;td&gt;Session summary, not per-event enrichment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AFTER MATCH SKIP&lt;/td&gt;
&lt;td&gt;SKIP PAST LAST ROW&lt;/td&gt;
&lt;td&gt;No overlapping sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PATTERN&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(A B{2,})&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Anchor A + 2 or more continuation rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DEFINE A&lt;/td&gt;
&lt;td&gt;TRUE&lt;/td&gt;
&lt;td&gt;Any row can start a session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DEFINE B&lt;/td&gt;
&lt;td&gt;Within 30 min of prior row&lt;/td&gt;
&lt;td&gt;Sessionization inactivity threshold&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Post-filter&lt;/td&gt;
&lt;td&gt;QUALIFY ROW_NUMBER() = 1&lt;/td&gt;
&lt;td&gt;First session per user&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The MR block finds every session of length ≥ 3 per user; the QUALIFY picks the earliest one per user. This is the sessionization + first-match composition pattern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;match_id&lt;/th&gt;
&lt;th&gt;session_start&lt;/th&gt;
&lt;th&gt;session_end&lt;/th&gt;
&lt;th&gt;event_count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2026-07-01 10:00&lt;/td&gt;
&lt;td&gt;2026-07-01 10:20&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2026-07-02 09:00&lt;/td&gt;
&lt;td&gt;2026-07-02 09:10&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;PATTERN (A B{2,}) encodes "anchor plus 2+ continuation"&lt;/strong&gt;&lt;/strong&gt; — the anchor &lt;code&gt;A&lt;/code&gt; matches any row (DEFINE A AS TRUE); the &lt;code&gt;B{2,}&lt;/code&gt; quantifier says "two or more continuation rows." Together, the pattern matches sessions of length ≥ 3 (one A + two or more B).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;DEFINE B references LAST(A.event_time) and LAST(B.event_time, 1)&lt;/strong&gt;&lt;/strong&gt; — the predicate says "this B row is within 30 minutes of the previous B row (or the A row if this is the first B)." &lt;code&gt;LAST(B.event_time, 1)&lt;/code&gt; is the "one B ago" — the immediately preceding B row inside the match.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;QUALIFY for first-per-user selection&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;QUALIFY ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY session_start) = 1&lt;/code&gt; picks the earliest session per user. QUALIFY is the Snowflake / BigQuery shorthand; on Oracle / Trino wrap it in a subquery with &lt;code&gt;WHERE rn = 1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;MATCH_NUMBER() in MEASURES&lt;/strong&gt;&lt;/strong&gt; — the per-partition match id is useful downstream for debugging and for joining back to the raw rows. Even when you don't display it, projecting it costs nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — one MR pass with a state-machine scan + one QUALIFY window pass. On a 1B-row event table with 10M users, the MR pass is O(N log N) for the sort + O(N) for the state machine; the QUALIFY is O(sessions). Fast on modern warehouses — single-digit minutes on a medium Snowflake warehouse.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — pattern matching&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Pattern matching drills&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/pattern-matching" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — regular expressions&lt;/span&gt;
&lt;strong&gt;Regular expression problems&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/regular-expressions" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  3. Sessionization with MR
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;sql pattern detection&lt;/code&gt; for event streams — the &lt;code&gt;PATTERN ((A B*)+)&lt;/code&gt; sessionization primitive with per-user partitioning and inactivity DEFINE
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;sessionization with MATCH_RECOGNIZE reads "anchor A, then zero-or-more continuation rows B where B is within 30 minutes of the previous B or the anchor A" — expressed as &lt;code&gt;PATTERN ((A B*)+)&lt;/code&gt; with a DEFINE predicate on B for the inactivity threshold&lt;/strong&gt;. Once you say "A is any row, B stays inside the threshold, and &lt;code&gt;(A B*)+&lt;/code&gt; produces one match per session," the MR sessionization interview surface reduces to a two-line PATTERN + DEFINE recipe.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcbkinusrbkpoorg6kc5s.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcbkinusrbkpoorg6kc5s.jpeg" alt="Visual diagram of MATCH_RECOGNIZE sessionization — left an event stream with per-user rows and inactivity gaps, a purple arrow labelled 'PATTERN ((A B*)+)' pointing right to a sessionized output with session_id, session_start, session_end, event_count; a small annotation about the 30-minute inactivity DEFINE predicate and per-user PARTITION BY; on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The canonical MR sessionization contract.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Definition.&lt;/strong&gt; A session is a maximal run of events from one user where consecutive events are separated by no more than the inactivity threshold. &lt;code&gt;PATTERN ((A B*)+)&lt;/code&gt; expresses this as "anchor + zero or more continuations, repeated one or more times." Each MR match is one session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anchor semantics.&lt;/strong&gt; &lt;code&gt;A&lt;/code&gt; matches any row; it's the session anchor. &lt;code&gt;DEFINE A AS TRUE&lt;/code&gt; is the standard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Continuation semantics.&lt;/strong&gt; &lt;code&gt;B&lt;/code&gt; matches a row within the inactivity threshold of the previous B row (or the anchor if this is the first B). &lt;code&gt;DEFINE B AS event_time - LAST(A.event_time) &amp;lt;= INTERVAL '30' MINUTE AND event_time - LAST(B.event_time, 1) &amp;lt;= INTERVAL '30' MINUTE&lt;/code&gt;. On simple engines the second clause reduces to &lt;code&gt;event_time - PREV(event_time) &amp;lt;= INTERVAL '30' MINUTE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Match maximality.&lt;/strong&gt; MR is greedy by default — it matches as many rows as possible. Sessions naturally end when the next row exceeds the threshold, and the match terminates at the last row that stayed inside.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Two equivalent PATTERNs.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;PATTERN ((A B*)+)&lt;/code&gt; — nested pattern: one or more (A + zero-or-more B) blocks. Rarely used in this form; the outer &lt;code&gt;+&lt;/code&gt; is redundant because AFTER MATCH SKIP handles the "next session" transition.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PATTERN (A B*)&lt;/code&gt; — the canonical form. One anchor A + zero or more continuations B. Each match is one session; AFTER MATCH SKIP PAST LAST ROW starts the next match at the next row.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Choosing the threshold.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;30 minutes.&lt;/strong&gt; The Google Analytics / Segment / Rudderstack / Snowplow / PostHog / Mixpanel default. Right for consumer web / mobile.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;60 minutes.&lt;/strong&gt; Right for long-attention apps — video streaming, IDEs, coding editors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;10 minutes.&lt;/strong&gt; Right for high-frequency contexts — IoT dashboards, trading terminals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom.&lt;/strong&gt; Plot inter-event gap histograms; pick the elbow at the 95th percentile.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Per-user partitioning is non-negotiable.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;PARTITION BY user_id&lt;/code&gt; inside the MR block is mandatory for multi-user data. Without it, the pattern engine matches across user boundaries — a session from u1 can absorb u2's next event if their timestamps are close.&lt;/li&gt;
&lt;li&gt;Multiple partition columns supported — &lt;code&gt;PARTITION BY user_id, device_id&lt;/code&gt; for per-(user, device) sessions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Watermark ceiling for streaming.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On Flink SQL / RisingWave, sessionization runs in streaming mode with a watermark. The MR block can only close a session when the watermark passes the last event's timestamp + the inactivity threshold — otherwise a late event could still extend the session.&lt;/li&gt;
&lt;li&gt;Batch MR doesn't need a watermark — the whole partition is available.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Session-level aggregations via MEASURES.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;FIRST(event_time)&lt;/code&gt; — session start.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LAST(event_time)&lt;/code&gt; — session end.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LAST(event_time) - FIRST(event_time)&lt;/code&gt; — session duration.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;COUNT(*)&lt;/code&gt; — event count.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;COUNT(A.*)&lt;/code&gt; — anchor count (should always be 1 for &lt;code&gt;PATTERN (A B*)&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;COUNT(B.*)&lt;/code&gt; — continuation count.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FIRST(page_url)&lt;/code&gt; — landing page.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LAST(page_url)&lt;/code&gt; — exit page.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common MR sessionization interview probes.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Why PATTERN (A B*) and not (A+)?"&lt;/strong&gt; — &lt;code&gt;A+&lt;/code&gt; uses one label with the same DEFINE for both anchor and continuation; the DEFINE would need to handle the "first row" case. &lt;code&gt;A B*&lt;/code&gt; cleanly separates anchor semantics from continuation semantics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"How do you handle the first row?"&lt;/strong&gt; — &lt;code&gt;A&lt;/code&gt; matches any row (&lt;code&gt;DEFINE A AS TRUE&lt;/code&gt;), so the first row of the partition is always the anchor of the first match.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"What if a user has no events?"&lt;/strong&gt; — no rows in the partition, no matches, no output. Fine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"How do you re-sessionize when late data arrives?"&lt;/strong&gt; — recompute the MR query over a rolling window. The pattern engine is deterministic given the input rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"MR sessionization vs LAG + SUM — which do you ship?"&lt;/strong&gt; — MR on Oracle / Snowflake / Flink / Trino / RisingWave for readability; portable LAG + SUM for multi-warehouse dbt projects.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — 30-minute sessionization on Snowflake
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The canonical Snowflake sessionization ask: given &lt;code&gt;events(user_id, event_time, event_name, page_url)&lt;/code&gt;, produce session summaries with MR. This is the query every senior candidate should be able to write from memory on Snowflake.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given &lt;code&gt;events(user_id, event_time, event_name, page_url)&lt;/code&gt; with a 30-minute inactivity threshold, use MATCH_RECOGNIZE to produce &lt;code&gt;(user_id, session_id, session_start, session_end, event_count, landing_page, exit_page)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;event_time&lt;/th&gt;
&lt;th&gt;event_name&lt;/th&gt;
&lt;th&gt;page_url&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-01 10:00&lt;/td&gt;
&lt;td&gt;page_view&lt;/td&gt;
&lt;td&gt;/home&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-01 10:05&lt;/td&gt;
&lt;td&gt;click&lt;/td&gt;
&lt;td&gt;/home&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-01 10:20&lt;/td&gt;
&lt;td&gt;page_view&lt;/td&gt;
&lt;td&gt;/pricing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-01 11:15&lt;/td&gt;
&lt;td&gt;page_view&lt;/td&gt;
&lt;td&gt;/home&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-01 11:20&lt;/td&gt;
&lt;td&gt;click&lt;/td&gt;
&lt;td&gt;/home&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="n"&gt;MATCH_RECOGNIZE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt;
  &lt;span class="n"&gt;MEASURES&lt;/span&gt;
    &lt;span class="n"&gt;MATCH_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;FIRST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;session_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;LAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;session_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;           &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;FIRST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;landing_page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;LAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;exit_page&lt;/span&gt;
  &lt;span class="n"&gt;ONE&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="n"&gt;PER&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt;
  &lt;span class="k"&gt;AFTER&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt; &lt;span class="n"&gt;SKIP&lt;/span&gt; &lt;span class="n"&gt;PAST&lt;/span&gt; &lt;span class="k"&gt;LAST&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt;
  &lt;span class="n"&gt;PATTERN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt; &lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;DEFINE&lt;/span&gt;
    &lt;span class="n"&gt;B&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;DATEDIFF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'minute'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;PARTITION BY user_id&lt;/code&gt; — per-user sessionization. Each user is independent.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ORDER BY event_time&lt;/code&gt; — rows walked in time order.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PATTERN (A B*)&lt;/code&gt; — one anchor A + zero or more continuations B. Each match is one session.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DEFINE B AS DATEDIFF('minute', LAG(event_time), event_time) &amp;lt;= 30&lt;/code&gt; — a continuation row is one within 30 minutes of the previous row (which may be A on the first B, or the previous B). Snowflake's &lt;code&gt;LAG&lt;/code&gt; inside DEFINE reads the previous row's value in the current match.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MEASURES&lt;/code&gt; — MATCH_NUMBER for session_id, FIRST/LAST for boundaries, COUNT for event count, FIRST/LAST of page_url for landing / exit page. &lt;code&gt;AFTER MATCH SKIP PAST LAST ROW&lt;/code&gt; starts the next match right after the last matched row.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;session_id&lt;/th&gt;
&lt;th&gt;session_start&lt;/th&gt;
&lt;th&gt;session_end&lt;/th&gt;
&lt;th&gt;event_count&lt;/th&gt;
&lt;th&gt;landing_page&lt;/th&gt;
&lt;th&gt;exit_page&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2026-07-01 10:00&lt;/td&gt;
&lt;td&gt;2026-07-01 10:20&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;/home&lt;/td&gt;
&lt;td&gt;/pricing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2026-07-01 11:15&lt;/td&gt;
&lt;td&gt;2026-07-01 11:20&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;/home&lt;/td&gt;
&lt;td&gt;/home&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; MR sessionization on Snowflake is one query — PATTERN (A B*), DEFINE B on inactivity, MEASURES for the boundaries. Reads more clearly than the LAG + CASE + SUM equivalent; ships faster if your warehouse supports it.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the LAG + SUM portable equivalent
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; For a multi-warehouse project, ship the portable LAG + SUM version. This is the "same query, no MR" answer for Postgres, BigQuery, SQL Server, MySQL, Redshift, Databricks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Rewrite the sessionization query without MATCH_RECOGNIZE.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt; (Same as above.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;flagged&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;page_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CASE&lt;/span&gt;
      &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
             &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'30 minutes'&lt;/span&gt;
        &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
        &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;new_session&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;sessionized&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;page_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;flagged&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;session_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;session_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;landing_page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;exit_page&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sessionized&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;flagged&lt;/code&gt; CTE — flag &lt;code&gt;new_session = 1&lt;/code&gt; when the inter-event gap exceeds 30 minutes, or when there's no previous event.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sessionized&lt;/code&gt; CTE — cumulative-sum the flag per user to produce a per-user session id.&lt;/li&gt;
&lt;li&gt;Final SELECT — group by (user_id, session_id) and aggregate to session summary. The FILTER clauses for landing / exit page pick the URL of the first / last event in the session.&lt;/li&gt;
&lt;li&gt;Landing / exit page trick — using a window function inside FILTER makes the aggregate pick the URL from the boundary row. Simpler alternative on Postgres 16+: &lt;code&gt;(ARRAY_AGG(page_url ORDER BY event_time))[1]&lt;/code&gt; for the first, &lt;code&gt;(ARRAY_AGG(page_url ORDER BY event_time DESC))[1]&lt;/code&gt; for the last.&lt;/li&gt;
&lt;li&gt;Runtime — three window passes + one aggregate. Same cost class as the MR version; the query is longer but portable to every warehouse.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt; (Same as above.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; MR is shorter; portable is longer but universally supported. For dbt projects that run on multiple warehouses, ship portable. For Snowflake-only projects, ship MR.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — MR sessionization with explicit sign-out
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The nuanced follow-up: some products close sessions on explicit user actions (sign-out, tab-close). Extend the PATTERN with a terminator label C that closes the session when a specific event fires.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given &lt;code&gt;events(user_id, event_time, event_name)&lt;/code&gt;, treat &lt;code&gt;event_name = 'sign_out'&lt;/code&gt; as an explicit session terminator. Use MR to produce session summaries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;event_time&lt;/th&gt;
&lt;th&gt;event_name&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;10:00&lt;/td&gt;
&lt;td&gt;page_view&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;10:05&lt;/td&gt;
&lt;td&gt;sign_out&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;10:07&lt;/td&gt;
&lt;td&gt;page_view&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;10:10&lt;/td&gt;
&lt;td&gt;click&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="n"&gt;MATCH_RECOGNIZE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt;
  &lt;span class="n"&gt;MEASURES&lt;/span&gt;
    &lt;span class="n"&gt;MATCH_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;FIRST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;session_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;LAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;session_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_count&lt;/span&gt;
  &lt;span class="n"&gt;ONE&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="n"&gt;PER&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt;
  &lt;span class="k"&gt;AFTER&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt; &lt;span class="n"&gt;SKIP&lt;/span&gt; &lt;span class="n"&gt;PAST&lt;/span&gt; &lt;span class="k"&gt;LAST&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt;
  &lt;span class="n"&gt;PATTERN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt; &lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;C&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;DEFINE&lt;/span&gt;
    &lt;span class="n"&gt;B&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;DATEDIFF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'minute'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
       &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;event_name&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s1"&gt;'sign_out'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;C&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'sign_out'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;PATTERN (A B* C?)&lt;/code&gt; — anchor A + zero-or-more continuations B + optional terminator C. The terminator is a sign-out event.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DEFINE B AS ... AND event_name != 'sign_out'&lt;/code&gt; — B continuations exclude sign-out events. The moment we hit a sign-out, B stops matching.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DEFINE C AS event_name = 'sign_out'&lt;/code&gt; — C matches only the sign-out event. &lt;code&gt;C?&lt;/code&gt; makes it optional — sessions without a sign-out still match.&lt;/li&gt;
&lt;li&gt;After a session with a sign-out C, &lt;code&gt;AFTER MATCH SKIP PAST LAST ROW&lt;/code&gt; moves past the sign-out to the next event. That next event becomes the anchor of the following session.&lt;/li&gt;
&lt;li&gt;Compared to the "LAG(event_name) = 'sign_out'" trick in the portable version, MR expresses the terminator cleanly with a labelled row class. Much more readable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;session_id&lt;/th&gt;
&lt;th&gt;session_start&lt;/th&gt;
&lt;th&gt;session_end&lt;/th&gt;
&lt;th&gt;event_count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;10:00&lt;/td&gt;
&lt;td&gt;10:05&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;10:07&lt;/td&gt;
&lt;td&gt;10:10&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For any product with explicit session-close semantics, add a terminator label to PATTERN with &lt;code&gt;?&lt;/code&gt; quantifier and a DEFINE predicate matching the closing event. The rest of the pipeline stays the same.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on MR sessionization at scale
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "You run a product with 500M daily events and 50M users on Snowflake. Design the nightly sessionization pipeline using MATCH_RECOGNIZE, compute per-session aggregates, and forward-fill the last non-null utm_campaign across sessions per user for last-touch attribution. Walk me through the query and the plan cost."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using MR sessionization + per-session aggregate + forward-fill
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Stage 1 — MR sessionize&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;sessionized&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
  &lt;span class="n"&gt;MATCH_RECOGNIZE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;
    &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt;
    &lt;span class="n"&gt;MEASURES&lt;/span&gt;
      &lt;span class="n"&gt;MATCH_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;
    &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="n"&gt;PER&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt;
    &lt;span class="k"&gt;AFTER&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt; &lt;span class="n"&gt;SKIP&lt;/span&gt; &lt;span class="n"&gt;PAST&lt;/span&gt; &lt;span class="k"&gt;LAST&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt;
    &lt;span class="n"&gt;PATTERN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt; &lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;DEFINE&lt;/span&gt;
      &lt;span class="n"&gt;B&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;DATEDIFF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'minute'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="c1"&gt;-- Stage 2 — per-session aggregate + last non-null campaign&lt;/span&gt;
&lt;span class="n"&gt;per_session&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;session_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;session_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ARRAY_AGG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;utm_campaign&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;utm_campaign&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;))[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;last_touch_campaign&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sessionized&lt;/span&gt;
  &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="c1"&gt;-- Stage 3 — forward-fill across sessions&lt;/span&gt;
&lt;span class="n"&gt;per_user_last_touch&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;session_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;session_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;last_touch_campaign&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;LAST_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last_touch_campaign&lt;/span&gt; &lt;span class="k"&gt;IGNORE&lt;/span&gt; &lt;span class="n"&gt;NULLS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;
      &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;session_start&lt;/span&gt;
      &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="n"&gt;UNBOUNDED&lt;/span&gt; &lt;span class="k"&gt;PRECEDING&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;CURRENT&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;user_last_touch_campaign&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;per_session&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;per_user_last_touch&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_start&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;What runs&lt;/th&gt;
&lt;th&gt;Approx rows out on 500M input&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;MR sessionization (ALL ROWS PER MATCH)&lt;/td&gt;
&lt;td&gt;500M rows enriched with session_id&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Per-session aggregate&lt;/td&gt;
&lt;td&gt;~100M sessions (assuming 5 events/session avg)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Forward-fill last_touch&lt;/td&gt;
&lt;td&gt;100M rows with per-user attribution&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The three-stage MR + aggregate + forward-fill pipeline sessionizes the stream, aggregates each session to a summary row, then propagates the most recent non-null campaign across the user's session history.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;session_id&lt;/th&gt;
&lt;th&gt;session_start&lt;/th&gt;
&lt;th&gt;last_touch_campaign&lt;/th&gt;
&lt;th&gt;user_last_touch_campaign&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2026-07-01&lt;/td&gt;
&lt;td&gt;google_ads&lt;/td&gt;
&lt;td&gt;google_ads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2026-07-05&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;google_ads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2026-07-10&lt;/td&gt;
&lt;td&gt;facebook_ads&lt;/td&gt;
&lt;td&gt;facebook_ads&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;ALL ROWS PER MATCH keeps raw rows for later aggregation&lt;/strong&gt;&lt;/strong&gt; — the MR block emits the raw event rows enriched with session_id via MATCH_NUMBER(). This is the cleanest way to sessionize when the downstream stage still needs per-event columns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Per-session aggregate collapses via GROUP BY session_id&lt;/strong&gt;&lt;/strong&gt; — standard SQL from here — GROUP BY (user_id, session_id) with MIN / MAX / COUNT. The &lt;code&gt;ARRAY_AGG(... ORDER BY event_time DESC) FILTER (WHERE ... IS NOT NULL)[1]&lt;/code&gt; idiom picks the most recent non-null value inside the session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;LAST_VALUE IGNORE NULLS for cross-session forward-fill&lt;/strong&gt;&lt;/strong&gt; — the ANSI-standard "carry the last non-null forward" primitive. The &lt;code&gt;ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW&lt;/code&gt; frame is required — without it, &lt;code&gt;LAST_VALUE&lt;/code&gt; reads the whole partition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Modular pipeline&lt;/strong&gt;&lt;/strong&gt; — Stage 1 is MR-heavy (fine on Snowflake); Stages 2 and 3 are portable SQL. If you later move to a portable-only warehouse, only Stage 1 needs a rewrite to LAG + SUM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — Stage 1 is O(N log N) per partition — the MR sort dominates. Stages 2 and 3 are O(sessions) — cheap. On 500M events with 50M users on a medium Snowflake warehouse, the pipeline runs in 15-30 minutes. Well inside a nightly window.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — streaming&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Streaming analytics drills&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/streaming" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — streaming (medium)&lt;/span&gt;
&lt;strong&gt;Streaming problems (medium)&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/streaming/difficulty/medium" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  4. Funnel &amp;amp; conversion patterns
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;funnel analysis sql&lt;/code&gt; reads like a regex — &lt;code&gt;PATTERN (SIGNUP VIEW+ CART CHECKOUT)&lt;/code&gt; collapses a four-step funnel into one MR block
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;funnel analysis with MATCH_RECOGNIZE encodes the step ordering directly in the PATTERN — &lt;code&gt;PATTERN (SIGNUP VIEW+ CART CHECKOUT)&lt;/code&gt; reads left-to-right as "signup then one-or-more product views then add-to-cart then checkout," and DEFINE gives each label its event-type predicate&lt;/strong&gt;. Once you say "PATTERN is the funnel shape, DEFINE is each step's event predicate, MEASURES emits the conversion metrics," the funnel interview surface reduces to a regex over your event stream.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu9damd2z4b6bkq31gkr6.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu9damd2z4b6bkq31gkr6.jpeg" alt="Visual diagram of MATCH_RECOGNIZE funnel and conversion — left an event stream of a signup to checkout funnel, a purple arrow labelled 'PATTERN (SIGNUP VIEW+ CART CHECKOUT)' pointing right to a per-user conversion output with time-to-convert and step counts; a small annotation about PERMUTE and drop-off attribution with UNMATCHED rows; on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The canonical funnel PATTERN.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;PATTERN (SIGNUP VIEW+ CART CHECKOUT)&lt;/code&gt; — strict left-to-right ordering. User must fire SIGNUP, then one or more VIEW events, then CART, then CHECKOUT. Any deviation breaks the match.&lt;/li&gt;
&lt;li&gt;Label naming — use event-type-suggestive labels (SIGNUP, VIEW, CART, CHECKOUT) rather than A, B, C, D. Reads better; downstream engineers get the semantics for free.&lt;/li&gt;
&lt;li&gt;Quantifiers matter — &lt;code&gt;VIEW+&lt;/code&gt; (one or more) requires at least one product view; &lt;code&gt;VIEW*&lt;/code&gt; (zero or more) allows the user to skip views; &lt;code&gt;VIEW{2,}&lt;/code&gt; requires at least two views.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The DEFINE predicates.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;DEFINE SIGNUP AS event_type = 'signup'&lt;/code&gt; — matches only signup events.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DEFINE VIEW AS event_type = 'view_product'&lt;/code&gt; — matches only product view events.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DEFINE CART AS event_type = 'add_to_cart'&lt;/code&gt; — matches only add-to-cart events.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DEFINE CHECKOUT AS event_type = 'checkout'&lt;/code&gt; — matches only checkout events.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Time constraints inside DEFINE.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sometimes the funnel has an SLA — "checkout must occur within 24 hours of signup." Add a time predicate to CHECKOUT: &lt;code&gt;DEFINE CHECKOUT AS event_type = 'checkout' AND event_time - FIRST(SIGNUP.event_time) &amp;lt;= INTERVAL '24' HOUR&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Cross-step SLAs — "the last view must be within 1 hour of add-to-cart": &lt;code&gt;DEFINE CART AS event_type = 'add_to_cart' AND event_time - LAST(VIEW.event_time) &amp;lt;= INTERVAL '1' HOUR&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;MEASURES — the funnel metrics payload.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;FIRST(SIGNUP.event_time) AS signup_ts&lt;/code&gt; — funnel entry ts.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FIRST(CHECKOUT.event_time) AS checkout_ts&lt;/code&gt; — funnel exit ts.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LAST(CHECKOUT.event_time) - FIRST(SIGNUP.event_time) AS time_to_convert&lt;/code&gt; — end-to-end conversion time.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;COUNT(VIEW.*) AS view_count&lt;/code&gt; — how many product views before add-to-cart.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;COUNT(*) AS total_events&lt;/code&gt; — total events in the funnel path.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FIRST(VIEW.product_id) AS first_product_viewed&lt;/code&gt; — which product started the exploration.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LAST(VIEW.product_id) AS last_product_viewed&lt;/code&gt; — which product ended the exploration (often the one added to cart).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;AFTER MATCH SKIP for funnels.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;AFTER MATCH SKIP PAST LAST ROW&lt;/code&gt; — one funnel completion per user, ending at the checkout. The user's post-checkout events don't count for the current funnel. Right for "how many users completed the funnel?" reports.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AFTER MATCH SKIP TO NEXT ROW&lt;/code&gt; — allow overlapping funnels (a user starts a new SIGNUP after a checkout, before the next SIGNUP). Rare; only when the pattern legitimately overlaps.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AFTER MATCH SKIP TO LAST CHECKOUT&lt;/code&gt; — same as &lt;code&gt;SKIP PAST LAST ROW&lt;/code&gt; since CHECKOUT is the terminal label.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;PERMUTE — unordered variants.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;PATTERN (PERMUTE(A, B, C, D))&lt;/code&gt; — matches A, B, C, D in any order. Oracle / Trino / Flink support PERMUTE; Snowflake does not (as of 2026).&lt;/li&gt;
&lt;li&gt;Use for onboarding funnels where the order between steps 2 and 3 doesn't matter — "verify_email, add_phone" is the same as "add_phone, verify_email."&lt;/li&gt;
&lt;li&gt;Rare but powerful — PERMUTE explodes to N! alternations under the hood, so keep the arity ≤ 4.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Drop-off attribution — &lt;code&gt;WITH UNMATCHED ROWS&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Standard funnel analysis reports both converters and non-converters. &lt;code&gt;ALL ROWS PER MATCH WITH UNMATCHED ROWS&lt;/code&gt; emits every input row, either as part of a match or as an unmatched row.&lt;/li&gt;
&lt;li&gt;Unmatched rows have &lt;code&gt;MATCH_NUMBER() = NULL&lt;/code&gt; and &lt;code&gt;CLASSIFIER() = NULL&lt;/code&gt;. Filter to those to identify drop-off events.&lt;/li&gt;
&lt;li&gt;Powerful for "at which step did most users drop off?" reports — group unmatched rows by their event_type.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common &lt;code&gt;funnel analysis sql&lt;/code&gt; interview probes.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Why is MR better than four self-joins?"&lt;/strong&gt; — the self-join version is O(N^4) if you're unlucky; MR is O(N log N) per partition. Correctness is also easier — no accidental duplicate rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"How do you handle users who repeat steps?"&lt;/strong&gt; — quantifiers. &lt;code&gt;VIEW+&lt;/code&gt; handles multiple views; &lt;code&gt;CART?&lt;/code&gt; handles optional cart.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"How do you enforce a time window between steps?"&lt;/strong&gt; — DEFINE predicates with &lt;code&gt;event_time - LAST(...)&lt;/code&gt; inside them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"How do you compute drop-off per step?"&lt;/strong&gt; — &lt;code&gt;WITH UNMATCHED ROWS&lt;/code&gt; + group unmatched by event_type.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"MR funnel vs LEAD chain — which do you ship?"&lt;/strong&gt; — MR on capable engines; LEAD chain (four LEAD calls) on portable stacks.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — signup → view → cart → checkout funnel
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The canonical funnel: given a user event stream, find every user who completed a four-step funnel. Emit one row per completing user with conversion time and view count.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given &lt;code&gt;events(user_id, event_time, event_type, product_id)&lt;/code&gt;, use MR to find every user who completed the signup → view+ → cart → checkout funnel. Emit &lt;code&gt;(user_id, signup_ts, checkout_ts, time_to_convert_min, view_count, first_product, cart_product)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;event_time&lt;/th&gt;
&lt;th&gt;event_type&lt;/th&gt;
&lt;th&gt;product_id&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;09:00&lt;/td&gt;
&lt;td&gt;signup&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;09:02&lt;/td&gt;
&lt;td&gt;view_product&lt;/td&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;09:05&lt;/td&gt;
&lt;td&gt;view_product&lt;/td&gt;
&lt;td&gt;102&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;09:08&lt;/td&gt;
&lt;td&gt;add_to_cart&lt;/td&gt;
&lt;td&gt;102&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;09:12&lt;/td&gt;
&lt;td&gt;checkout&lt;/td&gt;
&lt;td&gt;102&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;10:00&lt;/td&gt;
&lt;td&gt;signup&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;10:03&lt;/td&gt;
&lt;td&gt;view_product&lt;/td&gt;
&lt;td&gt;201&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;10:05&lt;/td&gt;
&lt;td&gt;pageview&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="n"&gt;MATCH_RECOGNIZE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt;
  &lt;span class="n"&gt;MEASURES&lt;/span&gt;
    &lt;span class="k"&gt;FIRST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SIGNUP&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;              &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;signup_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;LAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CHECKOUT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;             &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;checkout_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;DATEDIFF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'minute'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SIGNUP&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;LAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CHECKOUT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;time_to_convert_min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;VIEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                          &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;view_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;FIRST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;VIEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                 &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;first_product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;LAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CART&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;cart_product&lt;/span&gt;
  &lt;span class="n"&gt;ONE&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="n"&gt;PER&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt;
  &lt;span class="k"&gt;AFTER&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt; &lt;span class="n"&gt;SKIP&lt;/span&gt; &lt;span class="n"&gt;PAST&lt;/span&gt; &lt;span class="k"&gt;LAST&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt;
  &lt;span class="n"&gt;PATTERN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SIGNUP&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;CART&lt;/span&gt; &lt;span class="n"&gt;CHECKOUT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;DEFINE&lt;/span&gt;
    &lt;span class="n"&gt;SIGNUP&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'signup'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;VIEW&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'view_product'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CART&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'add_to_cart'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CHECKOUT&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'checkout'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;PARTITION BY user_id ORDER BY event_time&lt;/code&gt; — per-user pattern scope, time-ordered.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PATTERN (SIGNUP VIEW+ CART CHECKOUT)&lt;/code&gt; — strict left-to-right: signup, one or more views, cart, checkout.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DEFINE&lt;/code&gt; — each label matches a specific &lt;code&gt;event_type&lt;/code&gt;. Any deviation breaks the match — a user who sees a page_view between VIEWs doesn't match the strict pattern.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MEASURES&lt;/code&gt; — signup ts (FIRST of SIGNUP), checkout ts (LAST of CHECKOUT), time_to_convert (delta), view_count (COUNT of VIEW rows), first product viewed, product in cart.&lt;/li&gt;
&lt;li&gt;u1 matches; u2 fails (never got to add_to_cart or checkout). Only u1 appears in the output.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;signup_ts&lt;/th&gt;
&lt;th&gt;checkout_ts&lt;/th&gt;
&lt;th&gt;time_to_convert_min&lt;/th&gt;
&lt;th&gt;view_count&lt;/th&gt;
&lt;th&gt;first_product&lt;/th&gt;
&lt;th&gt;cart_product&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;09:00&lt;/td&gt;
&lt;td&gt;09:12&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;td&gt;102&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; MR funnel = one PATTERN with the strict step order + one DEFINE per step + MEASURES for the conversion metrics. Reads like the funnel diagram; ships as one query.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — funnel with drop-off attribution
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The standard funnel report needs both converters &lt;em&gt;and&lt;/em&gt; the drop-off point for non-converters. &lt;code&gt;ALL ROWS PER MATCH WITH UNMATCHED ROWS&lt;/code&gt; exposes every input row along with match metadata — the query below emits converters as matched rows and drop-off events as unmatched rows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Extend the funnel to expose drop-off events. For every user, emit either their conversion path (matched) or their last event before drop-off (unmatched with CLASSIFIER = NULL).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt; (Same as above.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="n"&gt;MATCH_RECOGNIZE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt;
  &lt;span class="n"&gt;MEASURES&lt;/span&gt;
    &lt;span class="n"&gt;MATCH_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;match_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CLASSIFIER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;matched_step&lt;/span&gt;
  &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="n"&gt;PER&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;UNMATCHED&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt;
  &lt;span class="k"&gt;AFTER&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt; &lt;span class="n"&gt;SKIP&lt;/span&gt; &lt;span class="n"&gt;PAST&lt;/span&gt; &lt;span class="k"&gt;LAST&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt;
  &lt;span class="n"&gt;PATTERN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SIGNUP&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;CART&lt;/span&gt; &lt;span class="n"&gt;CHECKOUT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;DEFINE&lt;/span&gt;
    &lt;span class="n"&gt;SIGNUP&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'signup'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;VIEW&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'view_product'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CART&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'add_to_cart'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CHECKOUT&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'checkout'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;ALL ROWS PER MATCH WITH UNMATCHED ROWS&lt;/code&gt; — emit every input row. Matched rows carry &lt;code&gt;match_id&lt;/code&gt; and &lt;code&gt;matched_step&lt;/code&gt;. Unmatched rows have NULL for both.&lt;/li&gt;
&lt;li&gt;Downstream aggregation groups unmatched rows by &lt;code&gt;event_type&lt;/code&gt; to see where drop-off happened: &lt;code&gt;SELECT event_type, COUNT(*) FROM ... WHERE match_id IS NULL GROUP BY event_type ORDER BY COUNT(*) DESC&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;For u2, the signup and view_product rows are unmatched — the pattern couldn't complete because there's no add_to_cart or checkout. Both are emitted with &lt;code&gt;matched_step = NULL&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;For u1, every event is emitted with the step it matched (SIGNUP, VIEW, VIEW, CART, CHECKOUT).&lt;/li&gt;
&lt;li&gt;This is exactly the shape a Sankey funnel diagram wants — one row per event, enriched with match id and step label.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;event_time&lt;/th&gt;
&lt;th&gt;event_type&lt;/th&gt;
&lt;th&gt;match_id&lt;/th&gt;
&lt;th&gt;matched_step&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;09:00&lt;/td&gt;
&lt;td&gt;signup&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;SIGNUP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;09:02&lt;/td&gt;
&lt;td&gt;view_product&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;VIEW&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;09:05&lt;/td&gt;
&lt;td&gt;view_product&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;VIEW&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;09:08&lt;/td&gt;
&lt;td&gt;add_to_cart&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;CART&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;09:12&lt;/td&gt;
&lt;td&gt;checkout&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;CHECKOUT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;10:00&lt;/td&gt;
&lt;td&gt;signup&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;10:03&lt;/td&gt;
&lt;td&gt;view_product&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;10:05&lt;/td&gt;
&lt;td&gt;pageview&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For funnel drop-off attribution, use &lt;code&gt;ALL ROWS PER MATCH WITH UNMATCHED ROWS&lt;/code&gt;. Unmatched rows are your drop-off population — filter and aggregate by event_type to identify the leaky step.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — PERMUTE for onboarding step order
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Onboarding funnels sometimes let users complete steps in any order — "verify_email, add_phone, complete_profile" can happen 1-2-3 or 2-1-3 or any permutation. PERMUTE inside PATTERN matches any ordering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given &lt;code&gt;events(user_id, event_time, event_type)&lt;/code&gt;, find every user who completed the three-step onboarding (&lt;code&gt;verify_email&lt;/code&gt;, &lt;code&gt;add_phone&lt;/code&gt;, &lt;code&gt;complete_profile&lt;/code&gt;) in any order. Emit &lt;code&gt;(user_id, first_step_ts, last_step_ts, total_time_min)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;event_time&lt;/th&gt;
&lt;th&gt;event_type&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;09:00&lt;/td&gt;
&lt;td&gt;verify_email&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;09:05&lt;/td&gt;
&lt;td&gt;add_phone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;09:08&lt;/td&gt;
&lt;td&gt;complete_profile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;10:00&lt;/td&gt;
&lt;td&gt;add_phone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;10:03&lt;/td&gt;
&lt;td&gt;complete_profile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;10:06&lt;/td&gt;
&lt;td&gt;verify_email&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="n"&gt;MATCH_RECOGNIZE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt;
  &lt;span class="n"&gt;MEASURES&lt;/span&gt;
    &lt;span class="k"&gt;FIRST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;first_step_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;LAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                          &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;last_step_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;DATEDIFF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'minute'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;LAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_time_min&lt;/span&gt;
  &lt;span class="n"&gt;ONE&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="n"&gt;PER&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt;
  &lt;span class="k"&gt;AFTER&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt; &lt;span class="n"&gt;SKIP&lt;/span&gt; &lt;span class="n"&gt;PAST&lt;/span&gt; &lt;span class="k"&gt;LAST&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt;
  &lt;span class="n"&gt;PATTERN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PERMUTE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EMAIL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PHONE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PROFILE&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="n"&gt;DEFINE&lt;/span&gt;
    &lt;span class="n"&gt;EMAIL&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'verify_email'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;PHONE&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'add_phone'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;PROFILE&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'complete_profile'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;PATTERN (PERMUTE(EMAIL, PHONE, PROFILE))&lt;/code&gt; — match EMAIL, PHONE, PROFILE in any order. Under the hood the engine expands to all 3! = 6 alternations: &lt;code&gt;(EMAIL PHONE PROFILE | EMAIL PROFILE PHONE | PHONE EMAIL PROFILE | PHONE PROFILE EMAIL | PROFILE EMAIL PHONE | PROFILE PHONE EMAIL)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Both u1 (1-2-3 order) and u2 (2-3-1 order) match.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FIRST(event_time)&lt;/code&gt; — the earliest event in the match (regardless of which label it was).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LAST(event_time)&lt;/code&gt; — the latest event in the match.&lt;/li&gt;
&lt;li&gt;PERMUTE is available on Oracle, Trino, Flink. Snowflake requires explicit alternation as of 2026 — write the 6 branches manually or use a portable fallback.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;first_step_ts&lt;/th&gt;
&lt;th&gt;last_step_ts&lt;/th&gt;
&lt;th&gt;total_time_min&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;09:00&lt;/td&gt;
&lt;td&gt;09:08&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;10:00&lt;/td&gt;
&lt;td&gt;10:06&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; PERMUTE handles unordered funnel steps declaratively — write PERMUTE(A, B, C) instead of a 6-way OR. Keep arity ≤ 4 (24 alternations); above that, the state machine gets expensive.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on multi-step funnel attribution
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "You run an e-commerce site and want a nightly report: for each user who completed the signup → view+ → cart → checkout funnel, emit (user_id, signup_ts, checkout_ts, time_to_convert, view_count, drop_off_step) — where drop_off_step is 'CONVERTED' for completers and the last step reached for non-completers. Walk me through the query."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using MR + WITH UNMATCHED ROWS + last-step aggregate
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;walk&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
  &lt;span class="n"&gt;MATCH_RECOGNIZE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;
    &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt;
    &lt;span class="n"&gt;MEASURES&lt;/span&gt;
      &lt;span class="n"&gt;MATCH_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;match_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;CLASSIFIER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;matched_step&lt;/span&gt;
    &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="n"&gt;PER&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;UNMATCHED&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt;
    &lt;span class="k"&gt;AFTER&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt; &lt;span class="n"&gt;SKIP&lt;/span&gt; &lt;span class="n"&gt;PAST&lt;/span&gt; &lt;span class="k"&gt;LAST&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt;
    &lt;span class="n"&gt;PATTERN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SIGNUP&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;CART&lt;/span&gt; &lt;span class="n"&gt;CHECKOUT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;DEFINE&lt;/span&gt;
      &lt;span class="n"&gt;SIGNUP&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'signup'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="k"&gt;VIEW&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'view_product'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;CART&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'add_to_cart'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;CHECKOUT&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'checkout'&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;per_user&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;matched_step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'CHECKOUT'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;checkout_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;matched_step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'SIGNUP'&lt;/span&gt;   &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;signup_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;matched_step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'VIEW'&lt;/span&gt;   &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;view_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;-- The last step reached in any run (matched or unmatched signup / view / cart)&lt;/span&gt;
    &lt;span class="k"&gt;CASE&lt;/span&gt;
      &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;matched_step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'CHECKOUT'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'CONVERTED'&lt;/span&gt;
      &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;matched_step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'CART'&lt;/span&gt;     &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'CART'&lt;/span&gt;
      &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;matched_step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'VIEW'&lt;/span&gt;     &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'VIEW'&lt;/span&gt;
      &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;matched_step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'SIGNUP'&lt;/span&gt;   &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'SIGNUP'&lt;/span&gt;
      &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'signup'&lt;/span&gt;   &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'SIGNUP_UNMATCHED'&lt;/span&gt;
      &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="s1"&gt;'NO_SIGNUP'&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;drop_off_step&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;walk&lt;/span&gt;
  &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;signup_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;checkout_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;DATEDIFF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'minute'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signup_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;checkout_ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;time_to_convert_min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;view_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;drop_off_step&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;per_user&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;What runs&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;walk&lt;/code&gt; — MR with WITH UNMATCHED ROWS&lt;/td&gt;
&lt;td&gt;Emit every event with match_id + matched_step (or NULLs)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;per_user&lt;/code&gt; — aggregate&lt;/td&gt;
&lt;td&gt;Per-user MIN(signup_ts), MAX(checkout_ts), COUNT(views), and the last step reached&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Final SELECT&lt;/td&gt;
&lt;td&gt;Compute time_to_convert; emit converters and non-converters uniformly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pipeline emits one row per user, tagged with either 'CONVERTED' (checkout reached) or the last step they reached before dropping off. Downstream Sankey / attribution reports read this shape directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;signup_ts&lt;/th&gt;
&lt;th&gt;checkout_ts&lt;/th&gt;
&lt;th&gt;time_to_convert_min&lt;/th&gt;
&lt;th&gt;view_count&lt;/th&gt;
&lt;th&gt;drop_off_step&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;09:00&lt;/td&gt;
&lt;td&gt;09:12&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;CONVERTED&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;10:00&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;VIEW&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u3&lt;/td&gt;
&lt;td&gt;11:00&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;SIGNUP_UNMATCHED&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;ALL ROWS PER MATCH WITH UNMATCHED ROWS drives the whole report&lt;/strong&gt;&lt;/strong&gt; — one MR pass gives us every event enriched with match_id + matched_step. Matched rows drive conversion metrics; unmatched rows drive drop-off attribution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;CLASSIFIER() as a step-level label&lt;/strong&gt;&lt;/strong&gt; — projecting CLASSIFIER() into a &lt;code&gt;matched_step&lt;/code&gt; column means downstream CASE-WHEN aggregates can pick out specific step timestamps and counts without joining back to the raw event stream.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;CASE ladder to derive drop_off_step&lt;/strong&gt;&lt;/strong&gt; — the falling-through CASE ordered from CHECKOUT down to SIGNUP finds the deepest step reached. Rows that never signed up bubble to 'NO_SIGNUP'; rows that signed up but never entered the pattern (say, no view_product) show 'SIGNUP_UNMATCHED'.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Unified converters + non-converters&lt;/strong&gt;&lt;/strong&gt; — the same rowset carries both populations. Downstream funnel dashboards filter or aggregate as needed without another query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — one MR pass (O(N log N) per partition for the sort + O(N) state-machine scan) + one GROUP BY per user (O(N)) + one final SELECT (O(users)). On a 1B-event / 20M-user warehouse, the pipeline runs in single-digit minutes on a well-sized Snowflake medium warehouse. Nightly report cadence is easy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — user funnel analysis&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;User funnel analysis problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/user-funnel-analysis" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — window functions&lt;/span&gt;
&lt;strong&gt;Window function drills&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/window-functions" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  5. Anomaly detection &amp;amp; dialect matrix
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;oracle match_recognize&lt;/code&gt; and &lt;code&gt;snowflake match_recognize&lt;/code&gt; shine on anomalies — V-shape / W-shape reversals, fraud bursts, and the streaming-vs-batch dialect matrix
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;anomaly detection with MATCH_RECOGNIZE reads "match a shape defined by consecutive DOWN then UP transitions (V-shape), or DOWN then UP then DOWN then UP (W-shape / double bottom), or a burst of N spend events inside a time window (fraud)" — expressed as &lt;code&gt;PATTERN (STRT DOWN+ UP+)&lt;/code&gt; or &lt;code&gt;PATTERN (SPEND{5,})&lt;/code&gt; with DEFINE using PREV / FIRST / LAST predicates&lt;/strong&gt;. Once you say "PATTERN + PREV / FIRST — that's the anomaly engine," the interview surface reduces to a shape-to-PATTERN translation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbjurb8sih0jg7d5s5n59.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbjurb8sih0jg7d5s5n59.jpeg" alt="Visual diagram of MATCH_RECOGNIZE anomaly detection and the dialect support matrix — left a stock V-shape / W-shape anomaly with the DOWN+ UP+ pattern annotated on a mini line chart, a small burst-fraud panel with PATTERN (SPEND{5,}) IN 60 SECONDS; right a 7-column dialect card comparing Oracle, Snowflake, Flink SQL, Trino, RisingWave, Postgres, BigQuery MATCH_RECOGNIZE support and the gaps-and-islands fallback; on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The classic V-shape reversal — Oracle's canonical example.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- V-shape: N down candles followed by N up candles&lt;/span&gt;
&lt;span class="n"&gt;PATTERN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;STRT&lt;/span&gt; &lt;span class="n"&gt;DOWN&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;UP&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;DEFINE&lt;/span&gt;
  &lt;span class="n"&gt;STRT&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;DOWN&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;PREV&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;UP&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PREV&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;STRT&lt;/code&gt; is the anchor — matches any row (the row before the first &lt;code&gt;DOWN&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DOWN+&lt;/code&gt; — one or more consecutive rows where the price fell below the previous row's price.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;UP+&lt;/code&gt; — one or more consecutive rows where the price rose above the previous row's price.&lt;/li&gt;
&lt;li&gt;Match = anchor + downtrend + uptrend = a V-shape reversal.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;W-shape / double bottom.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;PATTERN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;STRT&lt;/span&gt; &lt;span class="n"&gt;DOWN&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;UP&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;DOWN&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;UP&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;DEFINE&lt;/span&gt;
  &lt;span class="n"&gt;STRT&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;DOWN&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;PREV&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;UP&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PREV&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Twice the V-shape — two consecutive V-shapes make a W.&lt;/li&gt;
&lt;li&gt;Common in technical analysis for "double bottom" reversal setup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Fraud burst — &lt;code&gt;PATTERN (SPEND{5,})&lt;/code&gt; inside 60 seconds.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;PATTERN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SPEND&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,})&lt;/span&gt;
&lt;span class="n"&gt;DEFINE&lt;/span&gt;
  &lt;span class="n"&gt;SPEND&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'spend'&lt;/span&gt;
       &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SPEND&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'60'&lt;/span&gt; &lt;span class="k"&gt;SECOND&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;SPEND{5,}&lt;/code&gt; — five or more &lt;code&gt;SPEND&lt;/code&gt; events in a row.&lt;/li&gt;
&lt;li&gt;DEFINE constrains that all SPEND events must occur within 60 seconds of the first — a "5+ transactions in 1 minute" burst.&lt;/li&gt;
&lt;li&gt;Emit the boundaries + total amount + count. Downstream sends alerts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Streaming CEP — Flink SQL / RisingWave.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Flink SQL exposes MATCH_RECOGNIZE with streaming semantics via its CEP (Complex Event Processing) library. Rows arrive continuously; the state machine tracks partial matches and emits the moment a match completes.&lt;/li&gt;
&lt;li&gt;Watermark handling — Flink's watermark tells the pattern engine when it's safe to close a match. A pattern like &lt;code&gt;PATTERN (A B{3,})&lt;/code&gt; only emits after the watermark has advanced past the last matched row.&lt;/li&gt;
&lt;li&gt;RisingWave ships the same clause with streaming semantics — MR runs continuously, matches emit as they complete.&lt;/li&gt;
&lt;li&gt;The batch semantics on Oracle / Snowflake / Trino are a &lt;em&gt;snapshot&lt;/em&gt; of the same engine — same pattern, all data at once.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Dialect matrix (as of 2026).&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;th&gt;MATCH_RECOGNIZE&lt;/th&gt;
&lt;th&gt;PERMUTE&lt;/th&gt;
&lt;th&gt;AFTER MATCH SKIP variants&lt;/th&gt;
&lt;th&gt;Streaming&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Oracle 12c+&lt;/td&gt;
&lt;td&gt;YES&lt;/td&gt;
&lt;td&gt;YES&lt;/td&gt;
&lt;td&gt;ALL&lt;/td&gt;
&lt;td&gt;Batch&lt;/td&gt;
&lt;td&gt;First to ship, 2013; the reference implementation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snowflake&lt;/td&gt;
&lt;td&gt;YES&lt;/td&gt;
&lt;td&gt;NO (as of 2026)&lt;/td&gt;
&lt;td&gt;Most&lt;/td&gt;
&lt;td&gt;Batch&lt;/td&gt;
&lt;td&gt;GA since 2021; the most-used commercial MR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flink SQL&lt;/td&gt;
&lt;td&gt;YES&lt;/td&gt;
&lt;td&gt;YES&lt;/td&gt;
&lt;td&gt;Most&lt;/td&gt;
&lt;td&gt;Streaming (CEP)&lt;/td&gt;
&lt;td&gt;Watermark-driven; emits on match completion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trino / Presto&lt;/td&gt;
&lt;td&gt;YES&lt;/td&gt;
&lt;td&gt;YES&lt;/td&gt;
&lt;td&gt;ALL&lt;/td&gt;
&lt;td&gt;Batch&lt;/td&gt;
&lt;td&gt;Since Trino 361; used by Athena / Dremio&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RisingWave&lt;/td&gt;
&lt;td&gt;YES&lt;/td&gt;
&lt;td&gt;YES&lt;/td&gt;
&lt;td&gt;Most&lt;/td&gt;
&lt;td&gt;Streaming&lt;/td&gt;
&lt;td&gt;Since 1.7; PostgreSQL-compatible streaming DB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Postgres 16&lt;/td&gt;
&lt;td&gt;NO&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;No planned support; use LAG + SUM(new_group) OVER&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BigQuery&lt;/td&gt;
&lt;td&gt;NO&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;No planned support; use LAG + SUM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL Server 2022&lt;/td&gt;
&lt;td&gt;NO&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;No planned support; use LAG + SUM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL 8&lt;/td&gt;
&lt;td&gt;NO&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;No planned support; use LAG + SUM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redshift&lt;/td&gt;
&lt;td&gt;NO&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;No planned support; use LAG + SUM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Databricks SQL&lt;/td&gt;
&lt;td&gt;NO&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;No planned support (as of 2026); use LAG + SUM&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Portable fallback — LAG + SUM(new_group) OVER.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The universal fallback for MR — LAG for previous-row lookups, CASE to flag change points, SUM OVER to cumulative-sum into a run id, then GROUP BY run id to aggregate.&lt;/li&gt;
&lt;li&gt;Same computational complexity — O(N log N) per partition for the sort + O(N) for the CASE + SUM pass.&lt;/li&gt;
&lt;li&gt;Longer code — 20-40 lines vs 10-15 for MR. The gap grows with pattern complexity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Trino / Presto specifics.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Trino ships full ANSI MR since 361. AWS Athena inherits it from Trino. Dremio, Ahana, and Starburst all use the Trino MR implementation.&lt;/li&gt;
&lt;li&gt;Watch for the &lt;code&gt;SEEK&lt;/code&gt; variants — Trino's PATTERN parser is stricter than Oracle's; unquoted keywords may collide.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;RisingWave / Flink specifics.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Streaming MR requires an ORDER BY on an event-time attribute with a defined watermark. Without a watermark, the pattern engine can't decide when to close a match.&lt;/li&gt;
&lt;li&gt;Late data — rows arriving after the watermark are dropped (Flink) or reported to a side output (RisingWave configurable).&lt;/li&gt;
&lt;li&gt;Match emission — Flink emits on match completion; RisingWave has an option to emit progressive updates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common &lt;code&gt;sql pattern detection&lt;/code&gt; interview probes.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Which engines ship MR?"&lt;/strong&gt; — Oracle 12c+, Snowflake, Flink SQL, Trino / Presto, RisingWave. Not Postgres, BigQuery, SQL Server, MySQL, Redshift, Databricks SQL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Which engine has streaming MR?"&lt;/strong&gt; — Flink SQL and RisingWave.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Explain the V-shape PATTERN."&lt;/strong&gt; — anchor + DOWN+ + UP+; DEFINE DOWN AS price &amp;lt; PREV(price), UP AS price &amp;gt; PREV(price).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"How do you detect a fraud burst?"&lt;/strong&gt; — &lt;code&gt;PATTERN (SPEND{5,})&lt;/code&gt; + DEFINE using &lt;code&gt;event_time - FIRST(SPEND.event_time) &amp;lt;= INTERVAL '60' SECOND&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"What's the portable fallback?"&lt;/strong&gt; — LAG + CASE + SUM(new_group) OVER + GROUP BY run_id.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Worked example — V-shape reversal on Oracle / Snowflake
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The classic Oracle documentation example: find every V-shape reversal in a stock price series. Emit the run's boundary timestamps and prices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given &lt;code&gt;ticker(symbol, ts, price)&lt;/code&gt;, use MR to find every V-shape reversal per symbol (at least one down candle, at least one up candle after the trough).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;symbol&lt;/th&gt;
&lt;th&gt;ts&lt;/th&gt;
&lt;th&gt;price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AAPL&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AAPL&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;98&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AAPL&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;95&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AAPL&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;93&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AAPL&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;95&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AAPL&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AAPL&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;98&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AAPL&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;102&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ticker&lt;/span&gt;
&lt;span class="n"&gt;MATCH_RECOGNIZE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;
  &lt;span class="n"&gt;MEASURES&lt;/span&gt;
    &lt;span class="n"&gt;STRT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;start_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;LAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UP&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;end_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DOWN&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;trough_price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;LAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UP&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rebound_price&lt;/span&gt;
  &lt;span class="n"&gt;ONE&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="n"&gt;PER&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt;
  &lt;span class="k"&gt;AFTER&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt; &lt;span class="n"&gt;SKIP&lt;/span&gt; &lt;span class="n"&gt;PAST&lt;/span&gt; &lt;span class="k"&gt;LAST&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt;
  &lt;span class="n"&gt;PATTERN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;STRT&lt;/span&gt; &lt;span class="n"&gt;DOWN&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;UP&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;DEFINE&lt;/span&gt;
    &lt;span class="n"&gt;DOWN&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;PREV&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;UP&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PREV&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;PARTITION BY symbol&lt;/code&gt; — each stock's series is matched independently.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PATTERN (STRT DOWN+ UP+)&lt;/code&gt; — anchor + one-or-more downs + one-or-more ups. Matches a V-shape.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DEFINE DOWN AS price &amp;lt; PREV(price)&lt;/code&gt; — a row is DOWN if its price is less than the previous row's. &lt;code&gt;PREV(price)&lt;/code&gt; on the first row is NULL, so DOWN can't match the first row.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DEFINE UP AS price &amp;gt; PREV(price)&lt;/code&gt; — a row is UP if its price is greater than the previous row's.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MEASURES&lt;/code&gt; — &lt;code&gt;STRT.ts&lt;/code&gt; is the anchor's timestamp; &lt;code&gt;LAST(UP.ts)&lt;/code&gt; is the reversal's end; &lt;code&gt;MIN(DOWN.price)&lt;/code&gt; is the trough; &lt;code&gt;LAST(UP.price)&lt;/code&gt; is the rebound peak.&lt;/li&gt;
&lt;li&gt;Matches: row 1 (STRT) + rows 2-4 (DOWN+) + rows 5-6 (UP+) = one V-shape. Then row 7 → row 8 forms another V-shape starting from row 6 (STRT at 6, DOWN at 7, UP at 8).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;symbol&lt;/th&gt;
&lt;th&gt;start_ts&lt;/th&gt;
&lt;th&gt;end_ts&lt;/th&gt;
&lt;th&gt;trough_price&lt;/th&gt;
&lt;th&gt;rebound_price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AAPL&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;93&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AAPL&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;98&lt;/td&gt;
&lt;td&gt;102&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; V-shape reversal = &lt;code&gt;PATTERN (STRT DOWN+ UP+)&lt;/code&gt; + DEFINE DOWN / UP using &lt;code&gt;PREV(price)&lt;/code&gt;. The classic Oracle example — the query every financial-data interview loves.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — fraud burst detection on Flink SQL
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Flink SQL exposes MR as a streaming CEP operator. The fraud burst pattern — five or more spend events inside 60 seconds — matches as soon as the fifth spend arrives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; On Flink SQL, given a streaming &lt;code&gt;spend_events(user_id, event_time, amount)&lt;/code&gt; table with a defined watermark, use MR to emit an alert every time a user has 5+ spend events within 60 seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt; (Streaming — no snapshot table. Assume events arrive at 09:00:00, 09:00:05, 09:00:10, 09:00:20, 09:00:30, 09:00:45 for u1.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;spend_events&lt;/span&gt;
&lt;span class="n"&gt;MATCH_RECOGNIZE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt;
  &lt;span class="n"&gt;MEASURES&lt;/span&gt;
    &lt;span class="k"&gt;FIRST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SPEND&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;burst_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;LAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SPEND&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;burst_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;spend_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SPEND&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_amount&lt;/span&gt;
  &lt;span class="n"&gt;ONE&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="n"&gt;PER&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt;
  &lt;span class="k"&gt;AFTER&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt; &lt;span class="n"&gt;SKIP&lt;/span&gt; &lt;span class="n"&gt;PAST&lt;/span&gt; &lt;span class="k"&gt;LAST&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt;
  &lt;span class="n"&gt;PATTERN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SPEND&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,})&lt;/span&gt;
  &lt;span class="n"&gt;DEFINE&lt;/span&gt;
    &lt;span class="n"&gt;SPEND&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SPEND&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'60'&lt;/span&gt; &lt;span class="k"&gt;SECOND&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;PATTERN (SPEND{5,})&lt;/code&gt; — five or more consecutive SPEND rows.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DEFINE SPEND AS event_time - FIRST(SPEND.event_time) &amp;lt;= INTERVAL '60' SECOND&lt;/code&gt; — every SPEND row must be within 60 seconds of the &lt;em&gt;first&lt;/em&gt; SPEND in the current match. The moment a row exceeds the 60-second window, the match terminates.&lt;/li&gt;
&lt;li&gt;Because Flink uses streaming semantics, the state machine holds partial matches while waiting for more rows. It emits the moment the fifth row arrives and satisfies the pattern.&lt;/li&gt;
&lt;li&gt;Watermark handling — Flink advances the watermark based on the source event-time attribute. A match is only finalised once the watermark has moved past the last matched row (guaranteeing no late data can extend the match).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AFTER MATCH SKIP PAST LAST ROW&lt;/code&gt; — after emitting a burst, resume matching at the next row. A user who fires 10 spends inside 60 seconds emits one burst (the first 5+), then may emit another one starting from the 6th event.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt; (One row per completed burst.)&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;burst_start&lt;/th&gt;
&lt;th&gt;burst_end&lt;/th&gt;
&lt;th&gt;spend_count&lt;/th&gt;
&lt;th&gt;total_amount&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;09:00:00&lt;/td&gt;
&lt;td&gt;09:00:30&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;250&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Streaming fraud bursts on Flink SQL = &lt;code&gt;PATTERN (SPEND{n,})&lt;/code&gt; + DEFINE using a &lt;code&gt;FIRST(SPEND.event_time)&lt;/code&gt; time-window constraint. The engine emits the moment the pattern completes; the watermark guards against late data.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — portable LAG + SUM fallback on BigQuery
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; BigQuery doesn't ship MR. For the same V-shape reversal, write the portable LAG + SUM(new_group) OVER version. Longer, but universally supported.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Rewrite the V-shape reversal query for BigQuery (no MATCH_RECOGNIZE).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt; (Same as V-shape example.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;flagged&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CASE&lt;/span&gt;
      &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'DOWN'&lt;/span&gt;
      &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'UP'&lt;/span&gt;
      &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="s1"&gt;'FLAT'&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;direction&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ticker&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;runs&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;-- Start a new run when direction changes&lt;/span&gt;
    &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt;
          &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;direction&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
          &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;flagged&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;run_summary&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_min_price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_max_price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_length&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;runs&lt;/span&gt;
  &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;direction&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;paired&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;run_start&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;down_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;run_end&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;down_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;run_min_price&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;trough_price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;LEAD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;run_start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;next_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;LEAD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;run_start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;up_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;LEAD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;run_start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;up_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;LEAD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_max_price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;run_start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rebound_price&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;run_summary&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;direction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'DOWN'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;down_start&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;start_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;up_end&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;end_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;trough_price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;rebound_price&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;paired&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;next_dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'UP'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start_ts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;flagged&lt;/code&gt; CTE — label each row with &lt;code&gt;direction&lt;/code&gt; = DOWN / UP / FLAT relative to the previous row.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;runs&lt;/code&gt; CTE — cumulative-sum a "direction changed" flag to produce &lt;code&gt;run_id&lt;/code&gt;. Rows inside the same run share the id.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;run_summary&lt;/code&gt; CTE — collapse each run into a summary row with direction, boundaries, and min / max prices.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;paired&lt;/code&gt; CTE — pair consecutive runs by using LEAD to fetch the next run's direction and boundaries. A V-shape is a DOWN run immediately followed by an UP run.&lt;/li&gt;
&lt;li&gt;Final SELECT — filter to &lt;code&gt;direction = 'DOWN' AND next_dir = 'UP'&lt;/code&gt; and project the boundary columns.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt; (Same as the Oracle version.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; The portable fallback for V-shape reversal is four CTEs + one LEAD chain. Cost is O(N log N) per partition — same as MR. The code is longer; the query is universally supported.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on dialect-portable anomaly detection
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Your company runs analytics on Snowflake (batch, MR-capable) and BigQuery (batch, no MR). Design the anomaly-detection pipeline — V-shape reversals on a 100M-row daily price series — that produces the same output on both warehouses. Talk through the architecture and why you'd not use MR even on Snowflake."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using dbt macro with dialect dispatch + portable core
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- macros/detect_v_shape.sql — dispatches on target dialect&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;macro&lt;/span&gt; &lt;span class="n"&gt;detect_v_shape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prices_relation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;symbol_col&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'symbol'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts_col&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'ts'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price_col&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'price'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%-&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'snowflake'&lt;/span&gt; &lt;span class="o"&gt;-%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;-- Snowflake: use MATCH_RECOGNIZE for readability + native plan&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
      &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;symbol_col&lt;/span&gt; &lt;span class="p"&gt;}},&lt;/span&gt;
      &lt;span class="n"&gt;start_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;end_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;trough_price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;rebound_price&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;prices_relation&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
    &lt;span class="n"&gt;MATCH_RECOGNIZE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;symbol_col&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
      &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;ts_col&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
      &lt;span class="n"&gt;MEASURES&lt;/span&gt;
        &lt;span class="n"&gt;STRT&lt;/span&gt;&lt;span class="p"&gt;.{{&lt;/span&gt; &lt;span class="n"&gt;ts_col&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;          &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;start_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;LAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UP&lt;/span&gt;&lt;span class="p"&gt;.{{&lt;/span&gt; &lt;span class="n"&gt;ts_col&lt;/span&gt; &lt;span class="p"&gt;}})&lt;/span&gt;      &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;end_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DOWN&lt;/span&gt;&lt;span class="p"&gt;.{{&lt;/span&gt; &lt;span class="n"&gt;price_col&lt;/span&gt; &lt;span class="p"&gt;}})&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;trough_price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;LAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UP&lt;/span&gt;&lt;span class="p"&gt;.{{&lt;/span&gt; &lt;span class="n"&gt;price_col&lt;/span&gt; &lt;span class="p"&gt;}})&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rebound_price&lt;/span&gt;
      &lt;span class="n"&gt;ONE&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="n"&gt;PER&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt;
      &lt;span class="k"&gt;AFTER&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt; &lt;span class="n"&gt;SKIP&lt;/span&gt; &lt;span class="n"&gt;PAST&lt;/span&gt; &lt;span class="k"&gt;LAST&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt;
      &lt;span class="n"&gt;PATTERN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;STRT&lt;/span&gt; &lt;span class="n"&gt;DOWN&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;UP&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;DEFINE&lt;/span&gt;
        &lt;span class="n"&gt;DOWN&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;price_col&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;PREV&lt;/span&gt;&lt;span class="p"&gt;({{&lt;/span&gt; &lt;span class="n"&gt;price_col&lt;/span&gt; &lt;span class="p"&gt;}}),&lt;/span&gt;
        &lt;span class="n"&gt;UP&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;price_col&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PREV&lt;/span&gt;&lt;span class="p"&gt;({{&lt;/span&gt; &lt;span class="n"&gt;price_col&lt;/span&gt; &lt;span class="p"&gt;}})&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%-&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;-%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;-- Portable: LAG + SUM(new_group) OVER for BigQuery / Postgres / SQL Server / MySQL / Redshift / Databricks&lt;/span&gt;
    &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;flagged&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;SELECT&lt;/span&gt;
        &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;symbol_col&lt;/span&gt; &lt;span class="p"&gt;}},&lt;/span&gt;
        &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;ts_col&lt;/span&gt; &lt;span class="p"&gt;}},&lt;/span&gt;
        &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;price_col&lt;/span&gt; &lt;span class="p"&gt;}},&lt;/span&gt;
        &lt;span class="k"&gt;CASE&lt;/span&gt;
          &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;price_col&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;({{&lt;/span&gt; &lt;span class="n"&gt;price_col&lt;/span&gt; &lt;span class="p"&gt;}})&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;symbol_col&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;ts_col&lt;/span&gt; &lt;span class="p"&gt;}})&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'DOWN'&lt;/span&gt;
          &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;price_col&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;({{&lt;/span&gt; &lt;span class="n"&gt;price_col&lt;/span&gt; &lt;span class="p"&gt;}})&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;symbol_col&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;ts_col&lt;/span&gt; &lt;span class="p"&gt;}})&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'UP'&lt;/span&gt;
          &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="s1"&gt;'FLAT'&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;direction&lt;/span&gt;
      &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;prices_relation&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;runs&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;SELECT&lt;/span&gt;
        &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt;
              &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;direction&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;symbol_col&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;ts_col&lt;/span&gt; &lt;span class="p"&gt;}})&lt;/span&gt;
                &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;symbol_col&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;ts_col&lt;/span&gt; &lt;span class="p"&gt;}})&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
              &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
            &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;symbol_col&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;ts_col&lt;/span&gt; &lt;span class="p"&gt;}})&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;
      &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;flagged&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;run_summary&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;SELECT&lt;/span&gt;
        &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;symbol_col&lt;/span&gt; &lt;span class="p"&gt;}},&lt;/span&gt;
        &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;({{&lt;/span&gt; &lt;span class="n"&gt;ts_col&lt;/span&gt; &lt;span class="p"&gt;}})&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;({{&lt;/span&gt; &lt;span class="n"&gt;ts_col&lt;/span&gt; &lt;span class="p"&gt;}})&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;({{&lt;/span&gt; &lt;span class="n"&gt;price_col&lt;/span&gt; &lt;span class="p"&gt;}})&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_min_price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;({{&lt;/span&gt; &lt;span class="n"&gt;price_col&lt;/span&gt; &lt;span class="p"&gt;}})&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_max_price&lt;/span&gt;
      &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;runs&lt;/span&gt;
      &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;symbol_col&lt;/span&gt; &lt;span class="p"&gt;}},&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;direction&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;paired&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;SELECT&lt;/span&gt;
        &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;symbol_col&lt;/span&gt; &lt;span class="p"&gt;}},&lt;/span&gt;
        &lt;span class="n"&gt;run_start&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;start_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;run_min_price&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;trough_price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;LEAD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;symbol_col&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;run_start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;next_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;LEAD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;symbol_col&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;run_start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;end_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;LEAD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_max_price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;symbol_col&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;run_start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rebound_price&lt;/span&gt;
      &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;run_summary&lt;/span&gt;
      &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;direction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'DOWN'&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;symbol_col&lt;/span&gt; &lt;span class="p"&gt;}},&lt;/span&gt; &lt;span class="n"&gt;start_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;trough_price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rebound_price&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;paired&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;next_dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'UP'&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%-&lt;/span&gt; &lt;span class="n"&gt;endif&lt;/span&gt; &lt;span class="o"&gt;-%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;endmacro&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dispatch branch&lt;/th&gt;
&lt;th&gt;What runs&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Snowflake&lt;/td&gt;
&lt;td&gt;MATCH_RECOGNIZE PATTERN (STRT DOWN+ UP+)&lt;/td&gt;
&lt;td&gt;Native MR shorter, uses Snowflake's MR operator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BigQuery / Postgres / SQL Server / MySQL / Redshift / Databricks&lt;/td&gt;
&lt;td&gt;LAG + SUM(new_group) OVER + LEAD pair&lt;/td&gt;
&lt;td&gt;Portable fallback; universal coverage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Both&lt;/td&gt;
&lt;td&gt;Same output columns (symbol, start_ts, end_ts, trough_price, rebound_price)&lt;/td&gt;
&lt;td&gt;Downstream stays warehouse-agnostic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The macro dispatches on &lt;code&gt;target.type&lt;/code&gt; at compile time. Snowflake models get the native MR query; every other target gets the portable version. Both branches produce the same output schema, so downstream consumers are unchanged.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;symbol&lt;/th&gt;
&lt;th&gt;start_ts&lt;/th&gt;
&lt;th&gt;end_ts&lt;/th&gt;
&lt;th&gt;trough_price&lt;/th&gt;
&lt;th&gt;rebound_price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AAPL&lt;/td&gt;
&lt;td&gt;2026-07-01&lt;/td&gt;
&lt;td&gt;2026-07-05&lt;/td&gt;
&lt;td&gt;178&lt;/td&gt;
&lt;td&gt;195&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AAPL&lt;/td&gt;
&lt;td&gt;2026-07-08&lt;/td&gt;
&lt;td&gt;2026-07-12&lt;/td&gt;
&lt;td&gt;190&lt;/td&gt;
&lt;td&gt;205&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MSFT&lt;/td&gt;
&lt;td&gt;2026-07-02&lt;/td&gt;
&lt;td&gt;2026-07-06&lt;/td&gt;
&lt;td&gt;415&lt;/td&gt;
&lt;td&gt;430&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Compile-time dispatch, not runtime dispatch&lt;/strong&gt;&lt;/strong&gt; — dbt evaluates the Jinja block at compile time, so the emitted SQL is either a MR query or a LAG-based query — never a runtime branch. Zero overhead at query time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Same output columns across branches&lt;/strong&gt;&lt;/strong&gt; — both branches emit &lt;code&gt;(symbol, start_ts, end_ts, trough_price, rebound_price)&lt;/code&gt;. Downstream dashboards, reports, and joins don't need to know which warehouse produced the data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;MR is not always the right choice even where supported&lt;/strong&gt;&lt;/strong&gt; — some teams stick with the portable version even on Snowflake so the code review, the debug story, and the on-call ramp are the same everywhere. This macro gives the choice: opt into MR by adding a Snowflake branch, or ship portable everywhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Testability with the same fixture&lt;/strong&gt;&lt;/strong&gt; — write dbt tests against fixture data and run them on both warehouses. Same test file, same expected output — the macro guarantees output equivalence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — Snowflake's MR pass is O(N log N) per partition; the portable LAG + SUM pass is O(N log N) per partition. Same complexity class. On a 100M-row daily price table with 5000 symbols on Snowflake medium, both run in single-digit seconds; on BigQuery with the portable branch, similar (both use columnar storage + parallel window function operators). The dispatch macro adds zero runtime cost — the win is in maintainability, not performance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — window functions (SQL)&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Window functions in SQL — full library&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/window-functions/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;


&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — SQL&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL problem library — 450+ DE-focused questions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;





&lt;h2&gt;
  
  
  Cheat sheet — MATCH_RECOGNIZE recipe list
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Seven-clause skeleton.&lt;/strong&gt; &lt;code&gt;PARTITION BY&lt;/code&gt; (scope), &lt;code&gt;ORDER BY&lt;/code&gt; (row order), &lt;code&gt;MEASURES&lt;/code&gt; (per-match payload), &lt;code&gt;ONE ROW PER MATCH&lt;/code&gt; vs &lt;code&gt;ALL ROWS PER MATCH&lt;/code&gt; (output shape), &lt;code&gt;AFTER MATCH SKIP …&lt;/code&gt; (overlap policy), &lt;code&gt;PATTERN (...)&lt;/code&gt; (regex over row labels), &lt;code&gt;DEFINE &amp;lt;label&amp;gt; AS &amp;lt;predicate&amp;gt;&lt;/code&gt; (label semantics). Memorise the order; every MR answer fills these seven slots.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PATTERN is a regex over row labels.&lt;/strong&gt; Quantifiers: &lt;code&gt;*&lt;/code&gt; (zero or more), &lt;code&gt;+&lt;/code&gt; (one or more), &lt;code&gt;?&lt;/code&gt; (zero or one), &lt;code&gt;{n}&lt;/code&gt; (exactly n), &lt;code&gt;{n,m}&lt;/code&gt; (n to m). Alternation &lt;code&gt;|&lt;/code&gt;. Grouping &lt;code&gt;(...)&lt;/code&gt;. Reluctant quantifiers &lt;code&gt;+?&lt;/code&gt;, &lt;code&gt;*?&lt;/code&gt;, &lt;code&gt;??&lt;/code&gt; for shortest match.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DEFINE assigns predicates to labels.&lt;/strong&gt; Predicates can reference &lt;code&gt;PREV(col)&lt;/code&gt;, &lt;code&gt;NEXT(col)&lt;/code&gt;, &lt;code&gt;FIRST(label.col)&lt;/code&gt;, &lt;code&gt;LAST(label.col)&lt;/code&gt;, aggregates over matched labels, and the current row's columns. Any label used in PATTERN but not defined defaults to TRUE.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PARTITION BY is per-entity scoping.&lt;/strong&gt; Sessionization partitions by user_id; funnels partition by user_id; anomaly detection partitions by symbol. Omitting the partition matches across entity boundaries — a common wrong-answer bug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ORDER BY is mandatory.&lt;/strong&gt; Establishes what "previous" and "next" mean. Almost always the event time or a monotonic sequence. Missing ORDER BY is a compilation error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ONE ROW PER MATCH for summary; ALL ROWS PER MATCH for enrichment.&lt;/strong&gt; Session summaries use ONE ROW; per-event enrichment inside a match uses ALL ROWS. &lt;code&gt;ALL ROWS PER MATCH WITH UNMATCHED ROWS&lt;/code&gt; also emits unmatched rows — critical for funnel drop-off attribution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AFTER MATCH SKIP variants.&lt;/strong&gt; &lt;code&gt;SKIP PAST LAST ROW&lt;/code&gt; (default; no overlap; right for sessions, funnels, anomalies), &lt;code&gt;SKIP TO NEXT ROW&lt;/code&gt; (allow overlap; right for "every possible occurrence" analytics), &lt;code&gt;SKIP TO FIRST &amp;lt;label&amp;gt;&lt;/code&gt;, &lt;code&gt;SKIP TO LAST &amp;lt;label&amp;gt;&lt;/code&gt; (skip to a labelled row inside the match).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sessionization primitive.&lt;/strong&gt; &lt;code&gt;PATTERN (A B*)&lt;/code&gt; + &lt;code&gt;DEFINE B AS DATEDIFF('minute', LAG(event_time), event_time) &amp;lt;= 30&lt;/code&gt;. One anchor + zero-or-more continuations inside the inactivity threshold. One match per session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Funnel primitive.&lt;/strong&gt; &lt;code&gt;PATTERN (SIGNUP VIEW+ CART CHECKOUT)&lt;/code&gt; + one DEFINE per step. Strict left-to-right ordering; quantifiers on repeated steps. Encodes the funnel as regex; MR emits one row per completing user.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;V-shape / W-shape anomaly primitive.&lt;/strong&gt; &lt;code&gt;PATTERN (STRT DOWN+ UP+)&lt;/code&gt; for V; &lt;code&gt;PATTERN (STRT DOWN+ UP+ DOWN+ UP+)&lt;/code&gt; for W. DEFINE DOWN AS &lt;code&gt;price &amp;lt; PREV(price)&lt;/code&gt;, UP AS &lt;code&gt;price &amp;gt; PREV(price)&lt;/code&gt;. The classic Oracle documentation example — every financial-data interview loves this shape.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fraud burst primitive.&lt;/strong&gt; &lt;code&gt;PATTERN (SPEND{5,})&lt;/code&gt; + &lt;code&gt;DEFINE SPEND AS event_time - FIRST(SPEND.event_time) &amp;lt;= INTERVAL '60' SECOND&lt;/code&gt;. Five or more spend events inside a 60-second window from the first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CLASSIFIER() and MATCH_NUMBER().&lt;/strong&gt; &lt;code&gt;CLASSIFIER()&lt;/code&gt; returns the label the current row was classified into; &lt;code&gt;MATCH_NUMBER()&lt;/code&gt; is a monotonically-increasing per-partition match id. Project both into MEASURES when using ALL ROWS PER MATCH for downstream analytics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PERMUTE for unordered patterns.&lt;/strong&gt; &lt;code&gt;PATTERN (PERMUTE(A, B, C))&lt;/code&gt; matches A, B, C in any order — the engine expands to N! alternations under the hood. Oracle / Trino / Flink support PERMUTE; Snowflake requires manual alternation as of 2026.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WITH UNMATCHED ROWS for drop-off attribution.&lt;/strong&gt; &lt;code&gt;ALL ROWS PER MATCH WITH UNMATCHED ROWS&lt;/code&gt; emits both matched and unmatched rows. Unmatched rows have &lt;code&gt;MATCH_NUMBER() = NULL&lt;/code&gt; and &lt;code&gt;CLASSIFIER() = NULL&lt;/code&gt; — filter to those for drop-off events.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MR dialect matrix.&lt;/strong&gt; YES: Oracle 12c+, Snowflake, Flink SQL (streaming CEP), Trino / Presto, RisingWave. NO: Postgres, BigQuery, SQL Server, MySQL, Redshift, Databricks SQL (as of 2026). Streaming semantics only on Flink SQL and RisingWave.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Portable fallback — LAG + SUM(new_group) OVER.&lt;/strong&gt; For non-MR warehouses: &lt;code&gt;LAG(col)&lt;/code&gt; for previous value, &lt;code&gt;CASE WHEN col &amp;lt;&amp;gt; LAG(col) THEN 1 ELSE 0 END&lt;/code&gt; to flag change points, &lt;code&gt;SUM(is_new) OVER (PARTITION BY entity ORDER BY ts)&lt;/code&gt; for run id, then &lt;code&gt;GROUP BY run_id&lt;/code&gt; to aggregate. Same computational complexity, longer code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dbt macro dispatch pattern.&lt;/strong&gt; Wrap MR in a Jinja macro with &lt;code&gt;{% if target.type == 'snowflake' %}&lt;/code&gt; to emit the MR variant on Snowflake and the portable fallback everywhere else. Downstream consumers see identical output columns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flink streaming CEP.&lt;/strong&gt; Same MR syntax, watermark-driven. Match emits when the watermark advances past the last matched row. Late data is dropped or routed to a side output. Great for real-time fraud detection and low-latency alerting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When to use MR vs portable.&lt;/strong&gt; MR when the pattern is genuinely multi-row and regex-like (funnels, V/W anomalies, PERMUTE variants) and the warehouse supports it. Portable for simple RLE, sessionization on multi-warehouse projects, and any codebase where dialect drift is a maintenance burden.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost model.&lt;/strong&gt; MR compiles to &lt;code&gt;sort + state-machine scan&lt;/code&gt; — O(N log N) per partition for the sort + O(N) for the state machine. Portable LAG + SUM is &lt;code&gt;sort + two window passes + aggregate&lt;/code&gt; — same O(N log N) per partition. On billion-row inputs, both run in single-digit minutes on modern warehouses.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is SQL MATCH_RECOGNIZE and when do you use it?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;sql match_recognize&lt;/code&gt; is the ANSI SQL/2016 row pattern matching clause — a mini regex engine that runs over ordered rows inside a partition. The seven-clause skeleton is &lt;code&gt;PARTITION BY entity ORDER BY time MEASURES ... PATTERN (regex-over-labels) DEFINE label AS predicate ONE ROW PER MATCH AFTER MATCH SKIP …&lt;/code&gt;. Reach for it whenever you need to detect an ordered row sequence — sessionization (&lt;code&gt;PATTERN (A B*)&lt;/code&gt; with an inactivity DEFINE), funnel conversion (&lt;code&gt;PATTERN (SIGNUP VIEW+ CART CHECKOUT)&lt;/code&gt;), V-shape / W-shape stock anomaly (&lt;code&gt;PATTERN (STRT DOWN+ UP+)&lt;/code&gt;), fraud bursts (&lt;code&gt;PATTERN (SPEND{5,})&lt;/code&gt; inside a 60-second window), or any streaming CEP task where a Flink SQL / RisingWave watermark closes matches as they complete. MR ships on Oracle 12c+, Snowflake, Flink SQL, Trino / Presto, and RisingWave; Postgres, BigQuery, SQL Server, MySQL, Redshift, and Databricks SQL don't ship it yet, so multi-warehouse code defaults to the portable LAG + SUM(new_group) OVER fallback.&lt;/p&gt;

&lt;h3&gt;
  
  
  How does MATCH_RECOGNIZE compare to gaps-and-islands with LAG and SUM?
&lt;/h3&gt;

&lt;p&gt;Both express the same class of problems — sessionization, streaks, RLE, funnels, anomaly detection — but MATCH_RECOGNIZE reads as a regex over labelled row classes while gaps-and-islands cascades LAG, CASE, and SUM(is_new_group) OVER window functions. For simple RLE and sessionization, the two produce identical physical plans on Snowflake and roughly equivalent plans elsewhere; MR just reads more clearly once your team knows the anatomy. For complex multi-row patterns (V-shape / W-shape reversals, &lt;code&gt;PATTERN (SIGNUP VIEW+ CART CHECKOUT)&lt;/code&gt; funnels, PERMUTE variants, fraud bursts inside a time window) MR is dramatically shorter — 10-15 lines vs 30-40 lines portable, with the gap growing as the pattern gets more regex-like. The strategic answer for a multi-warehouse codebase is a dbt macro that dispatches on &lt;code&gt;target.type&lt;/code&gt;: emit MR on Snowflake / Oracle / Trino / Flink / RisingWave, emit the portable LAG + SUM version everywhere else, present the same output columns to downstream consumers.&lt;/p&gt;

&lt;h3&gt;
  
  
  What are the seven clauses of MATCH_RECOGNIZE?
&lt;/h3&gt;

&lt;p&gt;The seven slots are, in canonical reading order: (1) &lt;code&gt;PARTITION BY &amp;lt;cols&amp;gt;&lt;/code&gt; — scope the pattern per entity (user, symbol, device); (2) &lt;code&gt;ORDER BY &amp;lt;cols&amp;gt;&lt;/code&gt; — establish row order (event_time or a monotonic sequence); (3) &lt;code&gt;MEASURES &amp;lt;exprs&amp;gt;&lt;/code&gt; — per-match output columns using &lt;code&gt;FIRST(label.col)&lt;/code&gt;, &lt;code&gt;LAST(label.col)&lt;/code&gt;, &lt;code&gt;COUNT(*)&lt;/code&gt;, &lt;code&gt;MATCH_NUMBER()&lt;/code&gt;, and &lt;code&gt;CLASSIFIER()&lt;/code&gt;; (4) &lt;code&gt;ONE ROW PER MATCH&lt;/code&gt; for summary output vs &lt;code&gt;ALL ROWS PER MATCH&lt;/code&gt; (or &lt;code&gt;ALL ROWS PER MATCH WITH UNMATCHED ROWS&lt;/code&gt;) for per-event enrichment; (5) &lt;code&gt;AFTER MATCH SKIP …&lt;/code&gt; — the overlap policy (&lt;code&gt;PAST LAST ROW&lt;/code&gt; for no overlap, &lt;code&gt;TO NEXT ROW&lt;/code&gt; to allow overlap, &lt;code&gt;TO FIRST/LAST &amp;lt;label&amp;gt;&lt;/code&gt; for label-anchored skips); (6) &lt;code&gt;PATTERN (...)&lt;/code&gt; — the regex over row labels with quantifiers &lt;code&gt;* + ? {n} {n,m}&lt;/code&gt;, alternation &lt;code&gt;|&lt;/code&gt;, grouping &lt;code&gt;(...)&lt;/code&gt;, and &lt;code&gt;PERMUTE(...)&lt;/code&gt;; (7) &lt;code&gt;DEFINE &amp;lt;label&amp;gt; AS &amp;lt;predicate&amp;gt;&lt;/code&gt; — the boolean predicate for each label, allowed to reference &lt;code&gt;PREV&lt;/code&gt;, &lt;code&gt;NEXT&lt;/code&gt;, &lt;code&gt;FIRST&lt;/code&gt;, &lt;code&gt;LAST&lt;/code&gt;, and aggregates over already-matched labels. Say them in order without prompting and you're already in the top 20% of MR fluency.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you write a funnel-conversion query with MATCH_RECOGNIZE?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;funnel analysis sql&lt;/code&gt; with MATCH_RECOGNIZE reads like the funnel diagram itself: &lt;code&gt;PATTERN (SIGNUP VIEW+ CART CHECKOUT)&lt;/code&gt; encodes the strict left-to-right step ordering, &lt;code&gt;DEFINE SIGNUP AS event_type = 'signup'&lt;/code&gt;, &lt;code&gt;DEFINE VIEW AS event_type = 'view_product'&lt;/code&gt;, &lt;code&gt;DEFINE CART AS event_type = 'add_to_cart'&lt;/code&gt;, &lt;code&gt;DEFINE CHECKOUT AS event_type = 'checkout'&lt;/code&gt; assigns each label its event-type predicate, and MEASURES emits &lt;code&gt;FIRST(SIGNUP.event_time) AS signup_ts&lt;/code&gt;, &lt;code&gt;LAST(CHECKOUT.event_time) AS checkout_ts&lt;/code&gt;, &lt;code&gt;DATEDIFF('minute', ...) AS time_to_convert_min&lt;/code&gt;, and &lt;code&gt;COUNT(VIEW.*) AS view_count&lt;/code&gt;. Quantifiers control repetition (&lt;code&gt;VIEW+&lt;/code&gt; for one-or-more product views, &lt;code&gt;CART?&lt;/code&gt; for optional add-to-cart, &lt;code&gt;VIEW{2,}&lt;/code&gt; for at least two views). &lt;code&gt;PERMUTE(A, B, C)&lt;/code&gt; handles unordered onboarding funnels on Oracle / Trino / Flink. &lt;code&gt;ALL ROWS PER MATCH WITH UNMATCHED ROWS&lt;/code&gt; exposes drop-off events with &lt;code&gt;MATCH_NUMBER() = NULL&lt;/code&gt; for the standard Sankey funnel report — one MR pass, both converters and drop-off attribution.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you detect V-shape or W-shape anomalies in SQL?
&lt;/h3&gt;

&lt;p&gt;The classic &lt;code&gt;oracle match_recognize&lt;/code&gt; documentation example: a V-shape stock reversal is &lt;code&gt;PATTERN (STRT DOWN+ UP+)&lt;/code&gt; with &lt;code&gt;DEFINE DOWN AS price &amp;lt; PREV(price)&lt;/code&gt; and &lt;code&gt;DEFINE UP AS price &amp;gt; PREV(price)&lt;/code&gt; — one anchor row + one-or-more consecutive down candles + one-or-more consecutive up candles. &lt;code&gt;STRT.ts&lt;/code&gt; is the reversal's start; &lt;code&gt;LAST(UP.ts)&lt;/code&gt; is its end; &lt;code&gt;MIN(DOWN.price)&lt;/code&gt; is the trough; &lt;code&gt;LAST(UP.price)&lt;/code&gt; is the rebound. W-shape / double-bottom extends to &lt;code&gt;PATTERN (STRT DOWN+ UP+ DOWN+ UP+)&lt;/code&gt; — two consecutive V-shapes in a row. The same pattern engine runs the fraud-burst detection — &lt;code&gt;PATTERN (SPEND{5,})&lt;/code&gt; with &lt;code&gt;DEFINE SPEND AS event_time - FIRST(SPEND.event_time) &amp;lt;= INTERVAL '60' SECOND&lt;/code&gt; for a "5+ spends inside 60 seconds" alert. On &lt;code&gt;flink sql pattern&lt;/code&gt; and RisingWave, the same syntax runs continuously against a live event stream with watermark-driven match completion — the same anomaly recipe, streaming semantics. Portable fallback for warehouses without MR: LAG + CASE (DOWN / UP / FLAT direction label) + SUM(is_new_group) OVER for run id + LEAD to pair consecutive runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which SQL engines support MATCH_RECOGNIZE in 2026?
&lt;/h3&gt;

&lt;p&gt;As of 2026, MATCH_RECOGNIZE ships natively on &lt;strong&gt;Oracle 12c+&lt;/strong&gt; (first to market in 2013, the reference implementation), &lt;strong&gt;Snowflake&lt;/strong&gt; (batch, GA since 2021), &lt;strong&gt;Flink SQL&lt;/strong&gt; (streaming CEP semantics with watermark-driven match completion), &lt;strong&gt;Trino / Presto&lt;/strong&gt; (batch, since Trino 361 — inherited by AWS Athena, Dremio, Starburst, Ahana), and &lt;strong&gt;RisingWave&lt;/strong&gt; (streaming, since 1.7, PostgreSQL-compatible). No MR support on &lt;strong&gt;Postgres 16&lt;/strong&gt;, &lt;strong&gt;BigQuery&lt;/strong&gt;, &lt;strong&gt;SQL Server 2022&lt;/strong&gt;, &lt;strong&gt;MySQL 8&lt;/strong&gt;, &lt;strong&gt;Redshift&lt;/strong&gt;, or &lt;strong&gt;Databricks SQL&lt;/strong&gt; — for those engines, use the portable LAG + &lt;code&gt;SUM(new_group) OVER&lt;/code&gt; gaps-and-islands fallback. Streaming semantics only on Flink SQL and RisingWave; the other three (Oracle, Snowflake, Trino) are batch. PERMUTE is supported on Oracle, Trino, and Flink but not on Snowflake as of 2026. For a multi-warehouse codebase, wrap MR in a dbt macro with &lt;code&gt;{% if target.type == 'snowflake' or target.type == 'trino' %}&lt;/code&gt; dispatch and fall back to the portable pattern everywhere else — same output columns, same tests, one macro.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practice on PipeCode
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Drill the &lt;a href="https://pipecode.ai/explore/practice/topic/pattern-matching" rel="noopener noreferrer"&gt;pattern-matching practice library →&lt;/a&gt; for MATCH_RECOGNIZE anatomy, PATTERN quantifiers, DEFINE predicates, and AFTER MATCH SKIP semantics across Oracle, Snowflake, Trino, and Flink dialects.&lt;/li&gt;
&lt;li&gt;Rehearse on &lt;a href="https://pipecode.ai/explore/practice/topic/gaps-and-islands" rel="noopener noreferrer"&gt;gaps-and-islands problems →&lt;/a&gt; — the portable LAG + SUM(new_group) OVER fallback that every non-MR warehouse (Postgres, BigQuery, SQL Server, MySQL, Redshift, Databricks) requires.&lt;/li&gt;
&lt;li&gt;Sharpen &lt;a href="https://pipecode.ai/explore/practice/topic/streaming" rel="noopener noreferrer"&gt;streaming drills →&lt;/a&gt; for the Flink SQL and RisingWave CEP variants — watermark-driven pattern completion, low-latency emission, side outputs for late data.&lt;/li&gt;
&lt;li&gt;Push the difficulty ceiling with &lt;a href="https://pipecode.ai/explore/practice/topic/streaming/difficulty/medium" rel="noopener noreferrer"&gt;medium streaming problems →&lt;/a&gt; for real-time fraud detection, sessionization with watermarks, and multi-step funnel tracking.&lt;/li&gt;
&lt;li&gt;Practise &lt;a href="https://pipecode.ai/explore/practice/topic/window-functions" rel="noopener noreferrer"&gt;window-function drills →&lt;/a&gt; — the LAG / LEAD / FIRST_VALUE / LAST_VALUE primitives that both MR predicates and portable fallbacks lean on.&lt;/li&gt;
&lt;li&gt;Sharpen the dialect-portable half with &lt;a href="https://pipecode.ai/explore/practice/topic/window-functions/sql" rel="noopener noreferrer"&gt;SQL window function problems →&lt;/a&gt; for Postgres, Snowflake, BigQuery, SQL Server, MySQL where portable is the default.&lt;/li&gt;
&lt;li&gt;Layer &lt;a href="https://pipecode.ai/explore/practice/topic/user-funnel-analysis" rel="noopener noreferrer"&gt;user-funnel-analysis drills →&lt;/a&gt; for &lt;code&gt;PATTERN (SIGNUP VIEW+ CART CHECKOUT)&lt;/code&gt;, drop-off attribution via WITH UNMATCHED ROWS, and PERMUTE variants for unordered onboarding steps.&lt;/li&gt;
&lt;li&gt;Practise &lt;a href="https://pipecode.ai/explore/practice/topic/regular-expressions" rel="noopener noreferrer"&gt;regular-expression drills →&lt;/a&gt; — MATCH_RECOGNIZE's PATTERN clause is regex over rows; fluency with regex quantifiers transfers directly.&lt;/li&gt;
&lt;li&gt;For general SQL sharpening, work through the &lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;SQL problem library →&lt;/a&gt; which contains 450+ DE-focused questions.&lt;/li&gt;
&lt;li&gt;For the broader SQL interview surface, take the &lt;a href="https://pipecode.ai/explore/courses/sql-for-data-engineering-interviews-from-zero-to-faang" rel="noopener noreferrer"&gt;SQL for Data Engineering course →&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/" rel="noopener noreferrer"&gt;Pipecode.ai&lt;/a&gt; is Leetcode for Data Engineering — every `sql match_recognize` recipe above ships with hands-on practice rooms where you write the seven-clause anatomy, wire `PATTERN (A B*)` sessionization, chase the `PATTERN (SIGNUP VIEW+ CART CHECKOUT)` funnel with WITH UNMATCHED ROWS drop-off attribution, detect V-shape and W-shape reversals, rehearse the Flink SQL streaming CEP variant, and translate to the portable LAG + SUM(new_group) OVER fallback against real graded inputs. PipeCode pairs every reading with 450+ DE-focused problems and a real-time scoring engine, so you never have to wonder whether your `row pattern matching sql` answer holds up under a senior interviewer's depth probes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/pattern-matching" rel="noopener noreferrer"&gt;Practice pattern matching now →&lt;/a&gt;&lt;br&gt;
&lt;a href="https://pipecode.ai/explore/practice/topic/user-funnel-analysis" rel="noopener noreferrer"&gt;User funnel analysis drills →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>sql</category>
      <category>interview</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>SQL Gaps &amp; Islands: Sessionization, Streaks &amp; Run-Length Patterns</title>
      <dc:creator>Gowtham Potureddi</dc:creator>
      <pubDate>Fri, 10 Jul 2026 17:35:17 +0000</pubDate>
      <link>https://dev.to/gowthampotureddi/sql-gaps-islands-sessionization-streaks-run-length-patterns-3i0c</link>
      <guid>https://dev.to/gowthampotureddi/sql-gaps-islands-sessionization-streaks-run-length-patterns-3i0c</guid>
      <description>&lt;p&gt;&lt;strong&gt;&lt;code&gt;sql gaps and islands&lt;/code&gt;&lt;/strong&gt; is the single most-asked "consecutive rows" family in senior data-engineering interviews — and the single most under-taught one in undergraduate SQL curricula. The same "identify runs of consecutive events" ask shows up in five different colours: sessionization ("group events into 30-minute sessions"), login streaks ("longest consecutive-day streak per user"), uptime windows ("longest window with status = 'up'"), price plateaus ("compress the price series into runs of unchanged values"), and CDC state transitions ("emit one row per contiguous state"). Every answer starts from the same two-line trick: subtract a running counter from an ordered attribute, group by the difference, and each group is one island. The interviewer wants to hear you say "row_number minus date equals island id" in the first sentence.&lt;/p&gt;

&lt;p&gt;This guide is the mid-to-senior tour you wished existed the first time an interviewer asked you to write a &lt;code&gt;sessionization sql&lt;/code&gt; query on the whiteboard, sketch &lt;code&gt;sql streaks&lt;/code&gt; for a growth analytics dashboard, chase a &lt;code&gt;consecutive rows sql&lt;/code&gt; bug in production, derive a &lt;code&gt;sql session id&lt;/code&gt; for a Segment-style event stream, or design an &lt;code&gt;sql run length encoding&lt;/code&gt; compression for a status-transition table. It walks through the classic date-island Tabibitosan trick with &lt;code&gt;ROW_NUMBER&lt;/code&gt; and date arithmetic, the sessionization pattern with &lt;code&gt;LAG(event_time)&lt;/code&gt; plus &lt;code&gt;SUM(new_session) OVER&lt;/code&gt; and per-user partitioning, the LeetCode-flavoured consecutive-day streak problem with longest-vs-current framing and holiday tolerance, and the run-length encoding pattern with &lt;code&gt;SUM(CASE WHEN prev &amp;lt;&amp;gt; curr THEN 1 ELSE 0 END) OVER&lt;/code&gt; plus the MATCH_RECOGNIZE alternative and the Postgres / Snowflake / BigQuery / SQL Server dialect matrix. Every section pairs a teaching block with a Solution-Tail interview answer — code, a step-by-step trace, an output table, then a concept-by-concept breakdown of why it works.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbxf812zskbuq9ww58qy7.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbxf812zskbuq9ww58qy7.jpeg" alt="PipeCode blog header for SQL gaps and islands sessionization and streaks — bold white headline 'SQL Gaps &amp;amp; Islands' with subtitle 'Sessionization · Streaks · Run-Length Patterns' and a stylised island-id derivation card on a dark gradient with purple, green, orange, and blue accents and a small pipecode.ai attribution." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you want &lt;strong&gt;hands-on reps&lt;/strong&gt; immediately after reading, drill the &lt;a href="https://pipecode.ai/explore/practice/topic/gaps-and-islands" rel="noopener noreferrer"&gt;gaps-and-islands practice library →&lt;/a&gt;, rehearse on &lt;a href="https://pipecode.ai/explore/practice/topic/window-functions" rel="noopener noreferrer"&gt;window-function problems →&lt;/a&gt;, and sharpen the timing axis with &lt;a href="https://pipecode.ai/explore/practice/topic/time-series" rel="noopener noreferrer"&gt;time-series drills →&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;On this page&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why gaps &amp;amp; islands matters in 2026&lt;/li&gt;
&lt;li&gt;Classic date-island pattern&lt;/li&gt;
&lt;li&gt;Sessionization with 30-min inactivity&lt;/li&gt;
&lt;li&gt;Consecutive-day streaks&lt;/li&gt;
&lt;li&gt;Run-length encoding + dialect matrix&lt;/li&gt;
&lt;li&gt;Cheat sheet — Gaps &amp;amp; Islands recipe list&lt;/li&gt;
&lt;li&gt;Frequently asked questions&lt;/li&gt;
&lt;li&gt;Practice on PipeCode&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1. Why gaps &amp;amp; islands matters in 2026
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The "consecutive rows problem" family — sessionization, streaks, uptime, price plateaus, and every other run-of-events question a senior interviewer might ask
&lt;/h3&gt;

&lt;p&gt;The one-sentence invariant: &lt;strong&gt;a &lt;code&gt;sql gaps and islands&lt;/code&gt; question asks you to identify contiguous runs of rows that share some ordering property, and the canonical answer is always some form of "assign an island id, group by island id, aggregate per island"&lt;/strong&gt;. Once you internalise that "assign island id + group + aggregate" is the whole family, the consecutive-rows interview surface collapses into a two-step template you can write from muscle memory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Five faces of the same problem.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sessionization.&lt;/strong&gt; Given an event stream, group consecutive events into sessions separated by a gap of inactivity (usually 30 minutes). Every product analytics tool — Google Analytics, Segment, Rudderstack, Snowplow, PostHog, Mixpanel — implements exactly this logic. The interviewer wants to see you write it in warehouse SQL, not paste an out-of-the-box definition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streaks.&lt;/strong&gt; Given a table of user activity dates, find the longest consecutive-day streak per user (LeetCode's "Consecutive Numbers" and "Trips and Users" flavours). Growth teams live and die on daily-active retention, weekly streaks, workout streaks, learning-app streaks — all one query pattern.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uptime / downtime windows.&lt;/strong&gt; Given a table of status pings, find the longest window where &lt;code&gt;status = 'up'&lt;/code&gt; (or the last outage of duration &amp;gt; 5 minutes). SRE dashboards and pager duty policies both consume this shape.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price plateaus.&lt;/strong&gt; Given a price time series with many unchanged values in a row, compress into runs of &lt;code&gt;(start_ts, end_ts, price)&lt;/code&gt;. Storage cost drops by 10-100× for slowly-changing values.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CDC state transitions.&lt;/strong&gt; Given a &lt;code&gt;slowly changing dimension&lt;/code&gt; type-2 stream, emit one row per contiguous state. Debezium, Airbyte, and Fivetran output this shape; downstream models consume it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The two mental primitives.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Island id assignment.&lt;/strong&gt; The mechanical trick that turns a sequence into groups. Two flavours: (a) subtract a running counter from the ordered attribute (&lt;code&gt;login_date - ROW_NUMBER()&lt;/code&gt;); (b) cumulative-sum a "new group" flag (&lt;code&gt;SUM(CASE WHEN gap &amp;gt; threshold THEN 1 ELSE 0 END) OVER (...)&lt;/code&gt;). Every gaps-and-islands answer uses one of these two.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Group + aggregate.&lt;/strong&gt; Once you have an island id, &lt;code&gt;GROUP BY user_id, island_id&lt;/code&gt; and aggregate — &lt;code&gt;MIN(ts)&lt;/code&gt; as session start, &lt;code&gt;MAX(ts)&lt;/code&gt; as session end, &lt;code&gt;COUNT(*)&lt;/code&gt; as event count, etc. This half is boring; the whole trick is in the island-id derivation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why senior interviewers love this family.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It tests window-function fluency without being about ranking.&lt;/strong&gt; Everyone can write &lt;code&gt;ROW_NUMBER()&lt;/code&gt; for a leaderboard. Fewer candidates recognise that &lt;code&gt;ROW_NUMBER()&lt;/code&gt; also works as a gap-detection primitive when subtracted from an ordered attribute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It has a portable answer and a MATCH_RECOGNIZE answer.&lt;/strong&gt; The interviewer probes whether you know both — the portable window-function recipe and the Snowflake/Oracle native syntax — and when to reach for each.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's compositional.&lt;/strong&gt; Once you can sessionize, streaks are trivial. Once you can do streaks, RLE is a variation. Once you can do RLE, uptime windows are the same query with a filter. A senior candidate walks through this progression without prompting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It exposes multi-user partitioning discipline.&lt;/strong&gt; Every answer must &lt;code&gt;PARTITION BY user_id&lt;/code&gt; inside the window clauses. Candidates who forget the partition get wrong answers on the first test case — an instant red flag.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Where consecutive-rows queries land in your pipelines.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Product analytics.&lt;/strong&gt; Every event stream is sessionized before it can be counted, deduped, or attributed. &lt;code&gt;session_id&lt;/code&gt; is the atomic unit of product analytics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Growth analytics.&lt;/strong&gt; DAU/WAU/MAU retention, streak-based engagement metrics, funnel step deduplication — every metric that touches "consecutive days of activity" is a streaks query underneath.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliability / SRE.&lt;/strong&gt; Uptime SLOs, outage duration histograms, incident post-mortems — every timeline reconstruction is an islands query with a status attribute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Financial time series.&lt;/strong&gt; Price plateaus, volatility clusters, order book state transitions, market-hours vs after-hours splits — every "runs of contiguous values" question is RLE.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CDC / SCD pipelines.&lt;/strong&gt; Type-2 dimensions collapse consecutive unchanged rows into a single validity window using RLE. The &lt;code&gt;valid_from&lt;/code&gt; / &lt;code&gt;valid_to&lt;/code&gt; columns are computed exactly this way.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What senior interviewers actually probe.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Island-id derivation.&lt;/strong&gt; Do you reach for &lt;code&gt;ROW_NUMBER - date&lt;/code&gt; or &lt;code&gt;SUM(new_group) OVER&lt;/code&gt;? Do you know when to use each? Can you say "date arithmetic works when the sequence is dense; cumulative sum works when the gap is a computed threshold" without prompting?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sessionization threshold.&lt;/strong&gt; Do you hard-code &lt;code&gt;INTERVAL '30 minutes'&lt;/code&gt; or take it as a parameter? Do you know Segment / GA use 30 minutes by default? Do you handle timezone edge cases when the ETL runs across the DST boundary?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streak-with-gap tolerance.&lt;/strong&gt; Can you allow a 1-day miss without breaking the streak? Do you know how to pre-join a business-calendar table to exclude holidays and weekends from streak checks?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RLE compression math.&lt;/strong&gt; Can you estimate the compression ratio for a slowly-changing series? Do you know when RLE beats &lt;code&gt;DISTINCT ... LAG&lt;/code&gt; vs when a &lt;code&gt;DISTINCT&lt;/code&gt; view is enough?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MATCH_RECOGNIZE vs portable.&lt;/strong&gt; Do you know which warehouses ship MATCH_RECOGNIZE (Oracle, Snowflake) and which don't (Postgres, BigQuery, SQL Server)? Do you default to the portable LAG + SUM pattern for a multi-warehouse codebase?&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Worked example — the "consecutive numbers" starter problem
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The canonical whiteboard warm-up: given a &lt;code&gt;logs(id, num)&lt;/code&gt; table, find every number that appears at least three times consecutively. This is LeetCode's "Consecutive Numbers" problem, and it's the fastest way to check that a candidate recognises the gaps-and-islands family. The two-line answer uses &lt;code&gt;LAG&lt;/code&gt; twice or a &lt;code&gt;ROW_NUMBER - RANK&lt;/code&gt; trick, and the interviewer wants to see whichever variant you like walked through cleanly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given &lt;code&gt;logs(id, num)&lt;/code&gt; where &lt;code&gt;id&lt;/code&gt; is a dense ascending integer and &lt;code&gt;num&lt;/code&gt; is any integer, write a query that returns every &lt;code&gt;num&lt;/code&gt; that appears three or more times in a row.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;num&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Portable — LAG twice, check equality&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;num&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;ConsecutiveNums&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;num&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;num&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;prev1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;num&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;prev2&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;logs&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;num&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prev1&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;num&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prev2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;LAG(num, 1) OVER (ORDER BY id)&lt;/code&gt; produces the previous row's &lt;code&gt;num&lt;/code&gt;, ordered by &lt;code&gt;id&lt;/code&gt;. &lt;code&gt;LAG(num, 2) ...&lt;/code&gt; produces two rows back. Together, each row now knows its own value and the two before it.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;WHERE l.num = l.prev1 AND l.num = l.prev2&lt;/code&gt; predicate keeps only rows where the current value equals both the previous value and the one before that — a three-in-a-row match on the current row's timestamp.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SELECT DISTINCT l.num&lt;/code&gt; deduplicates the matches. If the same value appears in four consecutive rows, the predicate matches on rows 3 and 4; &lt;code&gt;DISTINCT&lt;/code&gt; collapses the duplicate output.&lt;/li&gt;
&lt;li&gt;The alternative form uses &lt;code&gt;num - ROW_NUMBER() OVER (PARTITION BY num ORDER BY id)&lt;/code&gt; — the island-id trick — then groups by &lt;code&gt;(num, island_id)&lt;/code&gt; and filters for &lt;code&gt;COUNT(*) &amp;gt;= 3&lt;/code&gt;. Same answer, different mechanics.&lt;/li&gt;
&lt;li&gt;Rule of thumb: for a fixed-length "N in a row" check, &lt;code&gt;LAG&lt;/code&gt; chains are more readable. For a variable-length "runs of X" query, the island-id trick generalises better.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ConsecutiveNums&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; LeetCode "Consecutive Numbers" is the gaps-and-islands warm-up — recognise it in 5 seconds, write the LAG chain in 30, then move on to the harder island-id derivation the interviewer really wants.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — sessionize an event stream (preview)
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The sessionization interview is a gaps-and-islands question wearing a product-analytics costume. The pattern is: compute the time gap to the previous event with &lt;code&gt;LAG(event_time)&lt;/code&gt;, flag rows where the gap exceeds a threshold, cumulative-sum the flag to produce a session id. This preview walks the mechanics; Section 3 goes deep on late-data and per-user partitioning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given an &lt;code&gt;events(user_id, event_time)&lt;/code&gt; stream and a 30-minute inactivity threshold, write a query that assigns a &lt;code&gt;session_id&lt;/code&gt; (integer, 0-indexed within each user) to every row.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;event_time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-01 10:00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-01 10:05&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-01 11:15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-01 11:20&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;
    &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt;
    &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="n"&gt;UNBOUNDED&lt;/span&gt; &lt;span class="k"&gt;PRECEDING&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;CURRENT&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CASE&lt;/span&gt;
      &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;EXTRACT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EPOCH&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;
        &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;new_session&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Inner query — &lt;code&gt;LAG(event_time) OVER (PARTITION BY user_id ORDER BY event_time)&lt;/code&gt; yields the previous event's timestamp within the user. The first event of each user has &lt;code&gt;LAG = NULL&lt;/code&gt;, and the epoch difference is NULL — treated as &lt;code&gt;0&lt;/code&gt; by the CASE, so the first event stays in session 0.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;EXTRACT(EPOCH FROM (event_time - LAG(...)))&lt;/code&gt; converts the interval to seconds. &lt;code&gt;&amp;gt; 30 * 60&lt;/code&gt; is the 30-minute threshold — any gap larger flags a new session.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CASE WHEN ... THEN 1 ELSE 0 END&lt;/code&gt; produces a 0/1 flag column called &lt;code&gt;new_session&lt;/code&gt;. This is the "island-boundary" indicator.&lt;/li&gt;
&lt;li&gt;Outer query — &lt;code&gt;SUM(new_session) OVER (PARTITION BY user_id ORDER BY event_time)&lt;/code&gt; cumulatively sums the flags per user. The result increments by 1 at every gap larger than 30 minutes, producing a per-user 0-indexed session id.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW&lt;/code&gt; is the explicit window frame; some dialects default to it, but stating it explicitly avoids the "range vs rows" trap that trips senior candidates.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;event_time&lt;/th&gt;
&lt;th&gt;session_id&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-01 10:00&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-01 10:05&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-01 11:15&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-01 11:20&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Sessionization is &lt;code&gt;LAG + CASE + SUM OVER&lt;/code&gt; — three window primitives combined. Recognise the pattern in seconds; write the query in a minute.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — collapse a price series with RLE (preview)
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Run-length encoding on a price series compresses consecutive unchanged values into one row per run. This preview shows the SUM-of-change-points trick; Section 5 goes deep on dialect variations and MATCH_RECOGNIZE alternatives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given &lt;code&gt;prices(ts, symbol, price)&lt;/code&gt; where &lt;code&gt;price&lt;/code&gt; changes rarely, write a query that emits one row per (symbol, run) with the run's start_ts, end_ts, and price.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ts&lt;/th&gt;
&lt;th&gt;symbol&lt;/th&gt;
&lt;th&gt;price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;AAPL&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;AAPL&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;AAPL&lt;/td&gt;
&lt;td&gt;105&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;AAPL&lt;/td&gt;
&lt;td&gt;105&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;AAPL&lt;/td&gt;
&lt;td&gt;105&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;AAPL&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;flagged&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CASE&lt;/span&gt;
      &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
        &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;is_new_run&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;prices&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;grouped&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;is_new_run&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;flagged&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;start_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;end_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_length&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;grouped&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;start_ts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;flagged&lt;/code&gt; CTE — for each row, compare the current &lt;code&gt;price&lt;/code&gt; to the previous row's &lt;code&gt;price&lt;/code&gt; (via &lt;code&gt;LAG&lt;/code&gt;). If it differs (or if it's the first row), mark &lt;code&gt;is_new_run = 1&lt;/code&gt;; otherwise 0. The special-case for NULL handles the very first row of the partition.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;grouped&lt;/code&gt; CTE — cumulative-sum the &lt;code&gt;is_new_run&lt;/code&gt; flag per symbol. Every 1 increments the counter; every 0 preserves it. The result is a &lt;code&gt;run_id&lt;/code&gt; that's constant within one run and increments at each change point.&lt;/li&gt;
&lt;li&gt;Outer SELECT — group by &lt;code&gt;(symbol, price, run_id)&lt;/code&gt; to collapse each run into one row. &lt;code&gt;MIN(ts)&lt;/code&gt; is the run start, &lt;code&gt;MAX(ts)&lt;/code&gt; is the run end, &lt;code&gt;COUNT(*)&lt;/code&gt; is the run length.&lt;/li&gt;
&lt;li&gt;Including &lt;code&gt;price&lt;/code&gt; in the GROUP BY is mostly cosmetic — the run_id already implies the price — but keeps the plan simple and the intent explicit.&lt;/li&gt;
&lt;li&gt;Compression ratio: a series where 90% of rows are duplicates compresses ~10×. For slowly-changing prices or feature-flag rollouts, RLE cuts storage and query cost dramatically.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;symbol&lt;/th&gt;
&lt;th&gt;price&lt;/th&gt;
&lt;th&gt;start_ts&lt;/th&gt;
&lt;th&gt;end_ts&lt;/th&gt;
&lt;th&gt;run_length&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AAPL&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AAPL&lt;/td&gt;
&lt;td&gt;105&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AAPL&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; RLE = flag change points with &lt;code&gt;LAG + CASE&lt;/code&gt;, cumulative-sum to run_id, then &lt;code&gt;GROUP BY run_id&lt;/code&gt;. Same three-step template as sessionization, different threshold.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on the gaps-and-islands mental model
&lt;/h3&gt;

&lt;p&gt;A senior interviewer often opens with: "I hand you a table of user events over the last year and ask for a metric — pick one — that requires you to identify runs of consecutive activity. Walk me through your two-step template, and tell me what changes if the ask is sessionization vs streaks vs uptime windows."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using the "flag + cumulative sum" universal template
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Universal gaps-and-islands template — three steps

Step 1 — Order the rows within each partition.
  PARTITION BY &amp;lt;entity&amp;gt; ORDER BY &amp;lt;time-or-sequence&amp;gt;

Step 2 — Assign an island id (two flavours).
  Flavour A — dense sequence subtraction (streaks, dense-date islands):
    island_id = ordered_attribute - ROW_NUMBER() OVER (PARTITION BY entity ORDER BY ordered_attribute)

  Flavour B — cumulative sum of change flag (sessions, RLE, uptime windows):
    is_new = CASE WHEN &amp;lt;boundary condition&amp;gt; THEN 1 ELSE 0 END
    island_id = SUM(is_new) OVER (PARTITION BY entity ORDER BY ordered_attribute)

Step 3 — Group by island id and aggregate.
  GROUP BY entity, island_id
  aggregates → MIN(ts) AS start, MAX(ts) AS end, COUNT(*) AS length, ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ask&lt;/th&gt;
&lt;th&gt;Flavour&lt;/th&gt;
&lt;th&gt;Boundary condition&lt;/th&gt;
&lt;th&gt;Aggregate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Login streaks&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;consecutive dates (dense)&lt;/td&gt;
&lt;td&gt;MIN(date), MAX(date), COUNT(*) as streak length&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sessionization&lt;/td&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;gap &amp;gt; 30 minutes&lt;/td&gt;
&lt;td&gt;MIN(ts) as session_start, COUNT(*) as event count&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uptime windows&lt;/td&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;status changes from &lt;code&gt;up&lt;/code&gt; to &lt;code&gt;down&lt;/code&gt; or vice versa&lt;/td&gt;
&lt;td&gt;MIN(ts), MAX(ts), duration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Price plateaus (RLE)&lt;/td&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;price &amp;lt;&amp;gt; LAG(price)&lt;/td&gt;
&lt;td&gt;MIN(ts), MAX(ts), price, COUNT(*) as run length&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consecutive-N check&lt;/td&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;value = LAG(value)&lt;/td&gt;
&lt;td&gt;filter to COUNT(*) &amp;gt;= N&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The universal template turns "which query do I write?" from a taste question into a mechanical checklist. Pick the partition, pick the ordering, pick the flavour, apply the aggregate — the whole query falls out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ask&lt;/th&gt;
&lt;th&gt;Query length&lt;/th&gt;
&lt;th&gt;Time to whiteboard&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Streak per user&lt;/td&gt;
&lt;td&gt;8-12 lines&lt;/td&gt;
&lt;td&gt;~2 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sessionization&lt;/td&gt;
&lt;td&gt;12-18 lines&lt;/td&gt;
&lt;td&gt;~3 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uptime windows&lt;/td&gt;
&lt;td&gt;15-25 lines&lt;/td&gt;
&lt;td&gt;~4 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Price RLE&lt;/td&gt;
&lt;td&gt;18-25 lines&lt;/td&gt;
&lt;td&gt;~4 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consecutive-N&lt;/td&gt;
&lt;td&gt;6-8 lines&lt;/td&gt;
&lt;td&gt;~1 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Two flavours cover the space&lt;/strong&gt;&lt;/strong&gt; — every gaps-and-islands question is either "dense sequence" (streaks, calendars) or "computed boundary" (sessions, RLE, uptime). Flavour A uses &lt;code&gt;ROW_NUMBER&lt;/code&gt; subtraction; Flavour B uses cumulative sum of a flag. There is no third pattern.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;PARTITION BY entity is non-negotiable&lt;/strong&gt;&lt;/strong&gt; — multi-user data always requires per-user partitioning. Skipping it produces a global island id that mixes users, which is a wrong-answer buffer that the interviewer catches immediately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;ORDER BY inside the window matters&lt;/strong&gt;&lt;/strong&gt; — the ordering determines what "consecutive" means. Sessionization orders by &lt;code&gt;event_time&lt;/code&gt;; RLE orders by &lt;code&gt;ts&lt;/code&gt;; streaks order by &lt;code&gt;date&lt;/code&gt;. Get the ordering wrong and the island id is meaningless.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;The aggregate is boring by design&lt;/strong&gt;&lt;/strong&gt; — once the island id is right, the aggregation is standard &lt;code&gt;GROUP BY + MIN + MAX + COUNT&lt;/code&gt;. Interviewers spend 90% of the follow-up time on island-id derivation and 10% on the aggregate — mirror that ratio in your explanation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — the template is O(N log N) per partition for the &lt;code&gt;ORDER BY&lt;/code&gt; inside the window, plus O(N) for the aggregate. On a 100M-row table with 1M users, that's ~1B operations — 10-30 seconds on modern warehouses. If it's too slow, pre-aggregate at ingestion or push partitions into a clustering key.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — gaps-and-islands&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Gaps and islands problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/gaps-and-islands" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — window functions&lt;/span&gt;
&lt;strong&gt;Window function drills&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/window-functions" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  2. Classic date-island pattern
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;consecutive rows sql&lt;/code&gt; in its purest form — the &lt;code&gt;date - ROW_NUMBER&lt;/code&gt; Tabibitosan trick that turns a dense sequence into island groups
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;when a dense-date sequence is punctuated by gaps, subtracting a per-partition &lt;code&gt;ROW_NUMBER()&lt;/code&gt; from the ordered attribute produces a constant per island — group by that constant and you have your runs&lt;/strong&gt;. Once you say "date minus row_number equals island id" out loud, the classic date-island interview surface collapses to a five-line SQL query.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpux697ii7dotn9ie7m5v.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpux697ii7dotn9ie7m5v.jpeg" alt="Visual diagram of the classic date-island pattern — left an input table of consecutive login dates with a row_number column, a purple arrow labelled 'date - row_number', right a two-island output table with island-id / first_day / last_day; a small annotation about the Tabibitosan trick; on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why the trick works.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dense sequences advance in lockstep.&lt;/strong&gt; If a user logs in on 2026-07-01, 2026-07-02, 2026-07-03, and the ordered &lt;code&gt;ROW_NUMBER()&lt;/code&gt; is 1, 2, 3, then &lt;code&gt;date - row_number&lt;/code&gt; is 2026-06-30 for all three rows. The difference is constant inside an island.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A gap breaks the lockstep.&lt;/strong&gt; If the next login is on 2026-07-06 and its &lt;code&gt;row_number&lt;/code&gt; is 4, then &lt;code&gt;date - row_number&lt;/code&gt; = 2026-07-02 — a different island id. The gap moved the row_number ahead of the date.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The island id has no meaning on its own — it's just a group key.&lt;/strong&gt; You never expose it in the output; you only use it in &lt;code&gt;GROUP BY&lt;/code&gt;. Aggregations recover the human-readable boundaries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Tabibitosan name.&lt;/strong&gt; The trick is credited to a Japanese SQL blog post by Aketi Jyuuzou circa 2008. "Tabibitosan" translates to "arithmetic problem for travellers," and refers to the classic maths puzzle "two travellers walking at different speeds meet in the same place — where and when?" — which is the same "lockstep + gap" mental model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dialect variations on the date arithmetic.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Postgres.&lt;/strong&gt; &lt;code&gt;login_date - (ROW_NUMBER() OVER (...) || ' days')::interval&lt;/code&gt; — cast the row number to an interval. Or &lt;code&gt;login_date - INTERVAL '1 day' * ROW_NUMBER() OVER (...)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snowflake.&lt;/strong&gt; &lt;code&gt;DATEADD(day, -ROW_NUMBER() OVER (...), login_date)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BigQuery.&lt;/strong&gt; &lt;code&gt;DATE_SUB(login_date, INTERVAL ROW_NUMBER() OVER (...) DAY)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL Server.&lt;/strong&gt; &lt;code&gt;DATEADD(day, -ROW_NUMBER() OVER (...), login_date)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MySQL 8.&lt;/strong&gt; &lt;code&gt;login_date - INTERVAL ROW_NUMBER() OVER (...) DAY&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Alternative: use integer difference for weekly or monthly islands.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Weekly islands: &lt;code&gt;WEEK(login_date) - ROW_NUMBER() OVER (...)&lt;/code&gt; — same trick, different granularity.&lt;/li&gt;
&lt;li&gt;Monthly islands: &lt;code&gt;MONTH(login_date) + YEAR(login_date) * 12 - ROW_NUMBER() OVER (...)&lt;/code&gt; — 12-month wrap.&lt;/li&gt;
&lt;li&gt;Custom period islands: any monotonically-increasing integer works. &lt;code&gt;EXTRACT(EPOCH FROM date)&lt;/code&gt; for second-granularity; &lt;code&gt;EXTRACT(EPOCH FROM date) / 3600&lt;/code&gt; for hour-granularity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Handling non-daily grids.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Weekends off.&lt;/strong&gt; Pre-filter to only Mon-Fri dates before applying the trick. Or pre-join a business-calendar table and use &lt;code&gt;business_day_number&lt;/code&gt; instead of &lt;code&gt;login_date&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Holidays off.&lt;/strong&gt; Same as weekends — pre-join a holiday calendar and use the &lt;code&gt;working_day_number&lt;/code&gt; as the ordered attribute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom activity days.&lt;/strong&gt; Any dense-integer attribute works. If a user must log in on the 1st, 15th, or 30th of each month to count, encode those dates into a dense sequence first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Multi-user partitioning.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Always &lt;code&gt;PARTITION BY user_id&lt;/code&gt; inside the &lt;code&gt;ROW_NUMBER()&lt;/code&gt; window. Without the partition, all users share one row_number and the island ids mix across users.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;GROUP BY&lt;/code&gt; after the subtraction must include &lt;code&gt;user_id&lt;/code&gt; too — same reason.&lt;/li&gt;
&lt;li&gt;The final SELECT always projects &lt;code&gt;user_id&lt;/code&gt; alongside the island columns.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Handling duplicates in the source data.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Distinct pre-step.&lt;/strong&gt; If a user can have multiple rows on the same date, &lt;code&gt;SELECT DISTINCT user_id, login_date FROM logins&lt;/code&gt; first. Otherwise, &lt;code&gt;ROW_NUMBER()&lt;/code&gt; sees duplicates as separate rows and breaks the lockstep.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;DENSE_RANK&lt;/code&gt;.&lt;/strong&gt; An alternative — &lt;code&gt;login_date - DENSE_RANK() OVER (PARTITION BY user_id ORDER BY login_date)&lt;/code&gt; — tolerates duplicates because &lt;code&gt;DENSE_RANK&lt;/code&gt; assigns the same rank to ties. Same island id for a duplicated date. Read &lt;code&gt;DENSE_RANK&lt;/code&gt; when the source may have duplicates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common &lt;code&gt;sql gaps and islands&lt;/code&gt; date-pattern gotchas.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wrong ORDER BY.&lt;/strong&gt; &lt;code&gt;ORDER BY login_date&lt;/code&gt; inside &lt;code&gt;ROW_NUMBER&lt;/code&gt; produces the right island ids only if the dates are dense. &lt;code&gt;ORDER BY user_id, login_date&lt;/code&gt; in a global window is a wrong-answer bug that mixes users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing PARTITION BY.&lt;/strong&gt; Global row_number across users mixes island ids. Every senior interviewer catches this on the second test case.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Off-by-one on the aggregation.&lt;/strong&gt; The island start is &lt;code&gt;MIN(login_date)&lt;/code&gt;; the end is &lt;code&gt;MAX(login_date)&lt;/code&gt;; the length is &lt;code&gt;MAX - MIN + 1&lt;/code&gt; (inclusive of both endpoints). Reporting &lt;code&gt;MAX - MIN&lt;/code&gt; under-counts by one — a classic streak-length bug.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — dense-date island detection
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The teaching problem: given a &lt;code&gt;logins(user_id, login_date)&lt;/code&gt; table where a user may log in on many consecutive days, find every user's login islands with start date, end date, and length. This is the canonical dense-date interview ask.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given &lt;code&gt;logins(user_id, login_date)&lt;/code&gt; with distinct rows per (user_id, login_date), write a query that returns &lt;code&gt;(user_id, island_start, island_end, island_length)&lt;/code&gt; for every login island of every user.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;login_date&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-01&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-02&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-03&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-06&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-07&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;2026-07-05&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;2026-07-06&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;numbered&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;logins&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;islanded&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;login_date&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rn&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;' days'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;island_id&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;numbered&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;island_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;island_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;island_length&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;islanded&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;island_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;island_start&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;numbered&lt;/code&gt; CTE — per user, order the login dates ascending and assign &lt;code&gt;rn = 1, 2, 3, ...&lt;/code&gt;. The partition ensures each user gets its own row_number sequence.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;islanded&lt;/code&gt; CTE — compute &lt;code&gt;island_id = login_date - rn days&lt;/code&gt;. For a consecutive run 2026-07-01, 2026-07-02, 2026-07-03 with rn = 1, 2, 3, the island_id is 2026-06-30 for all three rows. The next login on 2026-07-06 with rn = 4 gives island_id = 2026-07-02 — a different island.&lt;/li&gt;
&lt;li&gt;Outer SELECT — &lt;code&gt;GROUP BY user_id, island_id&lt;/code&gt; collapses each island into one row. &lt;code&gt;MIN(login_date)&lt;/code&gt; recovers the first date; &lt;code&gt;MAX(login_date)&lt;/code&gt; the last; &lt;code&gt;COUNT(*)&lt;/code&gt; the run length.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;ORDER BY user_id, island_start&lt;/code&gt; sorts the output for readability. Not strictly required — the caller can sort — but always nice on a whiteboard.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;::date&lt;/code&gt; cast defends against timezone drift when &lt;code&gt;login_date&lt;/code&gt; is a &lt;code&gt;timestamp&lt;/code&gt;; on a pure &lt;code&gt;date&lt;/code&gt; column, it's a no-op.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;island_start&lt;/th&gt;
&lt;th&gt;island_end&lt;/th&gt;
&lt;th&gt;island_length&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-01&lt;/td&gt;
&lt;td&gt;2026-07-03&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-06&lt;/td&gt;
&lt;td&gt;2026-07-07&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;2026-07-05&lt;/td&gt;
&lt;td&gt;2026-07-06&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Dense-date islands are three CTEs and one GROUP BY. Write the ROW_NUMBER, subtract from the date, group, aggregate — the whole pattern fits on a whiteboard in five minutes.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — handling weekend gaps (business-day islands)
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A common follow-up: "your streak query treats weekends as breaks — how do you make weekends not count?" The answer is to compress the dense-date grid down to only business days first, then apply the standard island-id trick against the compressed grid. This is where a pre-joined business-calendar table earns its keep.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given &lt;code&gt;logins(user_id, login_date)&lt;/code&gt; and a &lt;code&gt;business_days(date, business_day_num)&lt;/code&gt; calendar table where &lt;code&gt;business_day_num&lt;/code&gt; is a dense integer over Mon-Fri only, write a query that returns login islands treating weekends as "not a break."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input (logins).&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;login_date&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-06 (Mon)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-07 (Tue)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-10 (Fri)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-13 (Mon)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-14 (Tue)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Input (business_days).&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;date&lt;/th&gt;
&lt;th&gt;business_day_num&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2026-07-06&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-07-07&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-07-08&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-07-09&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-07-10&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-07-13&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-07-14&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;joined&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;business_day_num&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;logins&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;
  &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;business_days&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;login_date&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;numbered&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;business_day_num&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;joined&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;islanded&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;business_day_num&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;island_id&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;numbered&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;island_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;island_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;active_days&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;islanded&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;island_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;island_start&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;joined&lt;/code&gt; CTE — enrich each login with its &lt;code&gt;business_day_num&lt;/code&gt; from the calendar. Weekends are absent from the calendar, so any weekend login (if it exists) is dropped by the join. If you want weekend logins to still count as activity but not break the streak, adjust the join to be a left join and coalesce a synthetic business-day number.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;numbered&lt;/code&gt; CTE — same as before: per user, order by login_date and produce &lt;code&gt;rn&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;islanded&lt;/code&gt; CTE — &lt;code&gt;business_day_num - rn&lt;/code&gt; is now the island id. Because &lt;code&gt;business_day_num&lt;/code&gt; skips weekends, two logins on consecutive business days (Fri and Mon) have &lt;code&gt;business_day_num&lt;/code&gt; differing by 1 — the streak continues.&lt;/li&gt;
&lt;li&gt;Outer SELECT — group by island_id, take MIN/MAX/COUNT. &lt;code&gt;island_length&lt;/code&gt; here is renamed to &lt;code&gt;active_days&lt;/code&gt; to emphasise that it counts activity, not calendar days.&lt;/li&gt;
&lt;li&gt;Trade-off: the query now depends on a &lt;code&gt;business_days&lt;/code&gt; table. Every warehouse should have one — Snowflake's &lt;code&gt;SYSTEM$GENERATE_CALENDAR&lt;/code&gt;, dbt's &lt;code&gt;dbt_date&lt;/code&gt; package, or a hand-maintained table. If yours doesn't, generate one from &lt;code&gt;generate_series&lt;/code&gt; (Postgres) or a recursive CTE (SQL Server).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;island_start&lt;/th&gt;
&lt;th&gt;island_end&lt;/th&gt;
&lt;th&gt;active_days&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-06&lt;/td&gt;
&lt;td&gt;2026-07-10&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-13&lt;/td&gt;
&lt;td&gt;2026-07-14&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For any streak or island query where weekends or holidays should not break the run, pre-join a business-day calendar and use the calendar's dense integer as the ordered attribute. The trick is the same; the grid changes.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — first and last event per island
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A common variation: instead of aggregating an island to its length, ship the raw first and last events per island for downstream joins. This is what CDC pipelines do to compute type-2 slowly-changing dimension &lt;code&gt;valid_from&lt;/code&gt; / &lt;code&gt;valid_to&lt;/code&gt;. The trick is the same, plus a &lt;code&gt;FIRST_VALUE&lt;/code&gt; / &lt;code&gt;LAST_VALUE&lt;/code&gt; window pair to expose the endpoints on every row.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given &lt;code&gt;logins(user_id, login_date)&lt;/code&gt; and a derived island id, write a query that returns every login row enriched with &lt;code&gt;island_start&lt;/code&gt; and &lt;code&gt;island_end&lt;/code&gt; as extra columns, without collapsing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;login_date&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-01&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-02&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-03&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-06&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;numbered&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;logins&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;islanded&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;login_date&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rn&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;' days'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;island_id&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;numbered&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;island_id&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;island_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;island_id&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;island_end&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;islanded&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The first two CTEs are identical to the earlier example — produce &lt;code&gt;island_id&lt;/code&gt; per row using the row_number-subtracted-from-date trick.&lt;/li&gt;
&lt;li&gt;Outer SELECT — replace the &lt;code&gt;GROUP BY + MIN/MAX&lt;/code&gt; aggregation with &lt;code&gt;MIN() OVER (PARTITION BY user_id, island_id)&lt;/code&gt; and &lt;code&gt;MAX() OVER (...)&lt;/code&gt; — window aggregates that don't collapse the row.&lt;/li&gt;
&lt;li&gt;Every row now has three interesting columns: its own &lt;code&gt;login_date&lt;/code&gt;, the island's &lt;code&gt;start&lt;/code&gt;, and the island's &lt;code&gt;end&lt;/code&gt;. This shape is exactly what a downstream join wants — you can &lt;code&gt;JOIN ... USING (user_id, island_id)&lt;/code&gt; or &lt;code&gt;WHERE login_date = island_end&lt;/code&gt; to grab the "last event per island."&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;PARTITION BY user_id, island_id&lt;/code&gt; inside the window is the critical piece. Without both, either the users mix or the islands mix.&lt;/li&gt;
&lt;li&gt;Cost: two &lt;code&gt;OVER&lt;/code&gt; clauses over the same partition are typically fused into one window pass by modern planners. On a 100M row table, expect the same runtime as a single-window query.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;login_date&lt;/th&gt;
&lt;th&gt;island_start&lt;/th&gt;
&lt;th&gt;island_end&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-01&lt;/td&gt;
&lt;td&gt;2026-07-01&lt;/td&gt;
&lt;td&gt;2026-07-03&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-02&lt;/td&gt;
&lt;td&gt;2026-07-01&lt;/td&gt;
&lt;td&gt;2026-07-03&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-03&lt;/td&gt;
&lt;td&gt;2026-07-01&lt;/td&gt;
&lt;td&gt;2026-07-03&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-06&lt;/td&gt;
&lt;td&gt;2026-07-06&lt;/td&gt;
&lt;td&gt;2026-07-06&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; When you need the raw rows enriched with island boundaries — CDC, retention join, cohort assignment — swap the &lt;code&gt;GROUP BY + MIN/MAX&lt;/code&gt; for &lt;code&gt;MIN() OVER (PARTITION BY ..., island_id)&lt;/code&gt; window aggregates. Same island-id logic; different aggregation shape.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on multi-user island detection
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Given a &lt;code&gt;logins(user_id, login_date)&lt;/code&gt; table with 500M rows and 10M users, write the query that returns every user's longest login streak, and estimate the plan's cost. Show me both the standard Tabibitosan approach and how you'd optimise if the table is that large."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using per-user island id + aggregate + top-1 join
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Step 1 — assign island ids per user&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;numbered&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;logins&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;islanded&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;login_date&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rn&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;' days'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;island_id&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;numbered&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="c1"&gt;-- Step 2 — aggregate to island length per (user, island)&lt;/span&gt;
&lt;span class="n"&gt;per_island&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;island_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;island_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;island_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;streak_length&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;islanded&lt;/span&gt;
  &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;island_id&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="c1"&gt;-- Step 3 — rank per user, keep only the longest island&lt;/span&gt;
&lt;span class="n"&gt;ranked&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;island_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;island_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;streak_length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;streak_length&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;island_end&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;per_island&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;island_start&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;longest_streak_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;island_end&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;longest_streak_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;streak_length&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;longest_streak_days&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ranked&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;What runs&lt;/th&gt;
&lt;th&gt;Rows out (approx)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;ROW_NUMBER per user&lt;/td&gt;
&lt;td&gt;500M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;GROUP BY user_id, island_id&lt;/td&gt;
&lt;td&gt;~50M islands (assuming ~5 islands/user avg)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;ROW_NUMBER per user by streak length&lt;/td&gt;
&lt;td&gt;50M with rank&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Filter rn = 1&lt;/td&gt;
&lt;td&gt;10M (one per user)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The three-step pattern reduces 500M raw rows down to 10M output rows (one per user) with the longest streak per user. Every step is a standard window-function pass — no self-joins, no cross-joins, no correlated subqueries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;longest_streak_start&lt;/th&gt;
&lt;th&gt;longest_streak_end&lt;/th&gt;
&lt;th&gt;longest_streak_days&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-01&lt;/td&gt;
&lt;td&gt;2026-07-03&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;2026-05-15&lt;/td&gt;
&lt;td&gt;2026-05-19&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u3&lt;/td&gt;
&lt;td&gt;2026-06-10&lt;/td&gt;
&lt;td&gt;2026-06-10&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;PARTITION BY user_id everywhere&lt;/strong&gt;&lt;/strong&gt; — every window function in every CTE partitions by &lt;code&gt;user_id&lt;/code&gt;. Skipping it mixes users. The row_number, the min/max, and the rank all need the partition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Aggregation before ranking&lt;/strong&gt;&lt;/strong&gt; — the &lt;code&gt;per_island&lt;/code&gt; CTE collapses each island to one row before the final ranking. Ranking 500M row-level records is dramatically more expensive than ranking 50M island-level records.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Tiebreaker on island_end&lt;/strong&gt;&lt;/strong&gt; — if a user has two islands of the same maximum length, &lt;code&gt;ORDER BY streak_length DESC, island_end DESC&lt;/code&gt; keeps the more recent one. Interviewers appreciate seeing a tie-break argument even if the test data doesn't hit it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;ROW_NUMBER = 1 vs LIMIT&lt;/strong&gt;&lt;/strong&gt; — for the "top-1 per group" pattern, &lt;code&gt;ROW_NUMBER() OVER (PARTITION BY ...) = 1&lt;/code&gt; is the portable idiom. &lt;code&gt;DISTINCT ON (user_id)&lt;/code&gt; is a shorter Postgres-only equivalent; &lt;code&gt;QUALIFY ROW_NUMBER() = 1&lt;/code&gt; is Snowflake/BigQuery shorthand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — the plan is three window scans + one aggregate. On a 500M-row table with 10M users, the dominant cost is the first &lt;code&gt;ROW_NUMBER&lt;/code&gt; (sort-based, O(N log N) per partition). Clustering the table by &lt;code&gt;user_id&lt;/code&gt; reduces the sort cost per partition; on Snowflake, &lt;code&gt;CLUSTER BY (user_id, login_date)&lt;/code&gt; accelerates automatic clustering to keep this partition-friendly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — gaps-and-islands&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Gaps and islands drills&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/gaps-and-islands" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — window functions (medium)&lt;/span&gt;
&lt;strong&gt;Window function problems (SQL, medium)&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/window-functions/sql/medium" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  3. Sessionization with 30-min inactivity
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;sessionization sql&lt;/code&gt; is the flagship gaps-and-islands application — group event streams into sessions using LAG + SUM(new_session) OVER
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;sessionization turns an event stream into per-user sessions by flagging rows where the gap to the previous event exceeds an inactivity threshold, then cumulative-summing the flag to produce a per-user session id&lt;/strong&gt;. Once you say "LAG for the gap, CASE for the flag, SUM OVER for the id," the sessionization interview surface reduces to a three-line window recipe.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fljdl6hinn0ilya044nfc.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fljdl6hinn0ilya044nfc.jpeg" alt="Visual diagram of 30-minute sessionization — left an event stream with timestamps and inactivity gaps flagged, a purple arrow labelled 'LAG + SUM(new_session) OVER', right a sessionized output with session_id column; a small annotation about the 30-min inactivity rule and per-user partitioning; on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The canonical sessionization contract.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Definition.&lt;/strong&gt; A session is a maximal run of events from one user where consecutive events are separated by no more than the inactivity threshold. The first event of the day starts a new session; a gap greater than the threshold starts another new session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standard threshold.&lt;/strong&gt; 30 minutes. Google Analytics, Segment, Rudderstack, and Snowplow all default to 30. Some product tools default to 60 minutes for lower-frequency apps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session id.&lt;/strong&gt; Historically opaque — a UUID or a per-user monotonically-increasing integer. The integer form is what warehouse SQL usually produces; a UUID form is what real-time systems assign.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session boundaries.&lt;/strong&gt; &lt;code&gt;session_start_ts&lt;/code&gt; (first event ts), &lt;code&gt;session_end_ts&lt;/code&gt; (last event ts), &lt;code&gt;event_count&lt;/code&gt;, &lt;code&gt;session_duration_seconds&lt;/code&gt;, &lt;code&gt;first_page&lt;/code&gt;, &lt;code&gt;last_page&lt;/code&gt;, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The three-line pattern.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Step 1 — gap to previous event&lt;/span&gt;
&lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;prev_ts&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 2 — new-session flag when gap &amp;gt; threshold&lt;/span&gt;
&lt;span class="k"&gt;CASE&lt;/span&gt;
  &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;prev_ts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'30 minutes'&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;prev_ts&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
    &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;new_session&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 3 — cumulative-sum the flag to session id&lt;/span&gt;
&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chained together in a CTE or subquery, this is the whole primitive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choosing the threshold.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;30 min.&lt;/strong&gt; Google Analytics standard. Good for consumer web and mobile apps where a user goes to lunch or answers a Slack ping.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1 hour.&lt;/strong&gt; Loose. Good for long-attention apps like video streaming, coding editors, IDEs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;10 minutes.&lt;/strong&gt; Tight. Good for high-frequency apps like IoT device dashboards or trading terminals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom.&lt;/strong&gt; Some teams use a data-driven threshold — histogram all inter-event gaps and pick the elbow at the 95th percentile. If your sessions look wrong at 30 min, plot the histogram first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Timezone and DST handling.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Store &lt;code&gt;event_time&lt;/code&gt; as UTC. Sessionization operates on &lt;code&gt;event_time&lt;/code&gt; — the timezone of the user rarely matters for session boundaries.&lt;/li&gt;
&lt;li&gt;If the ETL runs across a DST fall-back (2 AM twice), sessions can wrap the double hour. Because we compare against UTC, this is invisible — always store UTC and problem solved.&lt;/li&gt;
&lt;li&gt;If a downstream report needs "session started before 6 PM local time" — convert &lt;code&gt;session_start_ts&lt;/code&gt; to the user's timezone at the report level, not in the sessionization step.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Late-arriving events.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real-world event streams have late events.&lt;/strong&gt; A mobile app buffers events offline and flushes when the network returns; a server-side event goes through a queue with variable latency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watermark.&lt;/strong&gt; Warehouse pipelines usually sessionize a rolling window (last 7 days) and re-sessionize on each run. Late events that arrive within the window are captured; older events are dropped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotent re-sessionization.&lt;/strong&gt; The window function is deterministic given the input rows — re-running the query produces the same session ids for the same rows. Downstream consumers see stable ids as long as the input is stable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UUID sessions.&lt;/strong&gt; If you need stable ids across ETL runs, hash &lt;code&gt;user_id&lt;/code&gt; and &lt;code&gt;session_start_ts&lt;/code&gt; to a UUID: &lt;code&gt;MD5(user_id || CAST(session_start_ts AS TEXT))&lt;/code&gt;. Deterministic, stable, but changes if the session start moves due to late data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Per-user partitioning is non-negotiable.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Global session id.&lt;/strong&gt; Wrong. Without &lt;code&gt;PARTITION BY user_id&lt;/code&gt; in &lt;code&gt;LAG&lt;/code&gt; and &lt;code&gt;SUM(...) OVER&lt;/code&gt;, events from different users can "close" each other's sessions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-user session id.&lt;/strong&gt; Right. &lt;code&gt;PARTITION BY user_id&lt;/code&gt; in every window function ensures session ids are computed independently per user.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-user attribution.&lt;/strong&gt; Some teams want a "global session ordinal" too — the Nth session across the whole system. That's a separate window pass; keep the per-user primitive clean first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Session-level aggregations.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once you have &lt;code&gt;session_id&lt;/code&gt;, aggregations are straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;session_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;session_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;session_duration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pageview'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;landing_page&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sessionized&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Common &lt;code&gt;sessionization sql&lt;/code&gt; interview probes.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Why LAG and not LEAD?"&lt;/strong&gt; — LAG references the previous event; the boundary flag fires when the &lt;em&gt;current&lt;/em&gt; event is far from the &lt;em&gt;previous&lt;/em&gt; one. LEAD would work symmetrically (fire on the last event of a session), but LAG is the canonical direction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"What if the first event has no previous?"&lt;/strong&gt; — &lt;code&gt;LAG&lt;/code&gt; returns NULL. Handle explicitly in the CASE (&lt;code&gt;OR prev_ts IS NULL&lt;/code&gt;) so the first event starts session 0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"How do you handle multi-user tables?"&lt;/strong&gt; — &lt;code&gt;PARTITION BY user_id&lt;/code&gt; everywhere. The interviewer wants to hear it in the first sentence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"How do you re-sessionize when late data arrives?"&lt;/strong&gt; — recompute over a rolling window (7 days or 30 days). Assume late events within the window; drop older ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"What's the difference between session id and session UUID?"&lt;/strong&gt; — the id is a per-user integer, the UUID is a global-unique identifier. Warehouse SQL usually produces the integer; downstream consumers convert to UUID via a deterministic hash.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Worked example — Segment-style event stream sessionization
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The most-asked sessionization ask in a product-analytics interview: given a Segment-style &lt;code&gt;events(user_id, event_time, event_name, page_url)&lt;/code&gt; stream, compute a &lt;code&gt;session_id&lt;/code&gt; per user and per-session aggregations. This is the core pipeline any Segment / Rudderstack / Snowplow user has written.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given &lt;code&gt;events(user_id, event_time, event_name, page_url)&lt;/code&gt; and a 30-minute inactivity threshold, produce a &lt;code&gt;sessions&lt;/code&gt; table with &lt;code&gt;(user_id, session_id, session_start, session_end, event_count, landing_page)&lt;/code&gt;. Show both the sessionization CTE and the aggregation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;event_time&lt;/th&gt;
&lt;th&gt;event_name&lt;/th&gt;
&lt;th&gt;page_url&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-01 10:00&lt;/td&gt;
&lt;td&gt;page_view&lt;/td&gt;
&lt;td&gt;/home&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-01 10:05&lt;/td&gt;
&lt;td&gt;click&lt;/td&gt;
&lt;td&gt;/home&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-01 10:20&lt;/td&gt;
&lt;td&gt;page_view&lt;/td&gt;
&lt;td&gt;/pricing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-01 11:15&lt;/td&gt;
&lt;td&gt;page_view&lt;/td&gt;
&lt;td&gt;/home&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-01 11:20&lt;/td&gt;
&lt;td&gt;click&lt;/td&gt;
&lt;td&gt;/home&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;flagged&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;page_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CASE&lt;/span&gt;
      &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'30 minutes'&lt;/span&gt;
        &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
        &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;new_session&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;sessionized&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;page_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;flagged&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;session_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;session_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;session_duration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;event_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'page_view'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;landing_page&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sessionized&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;flagged&lt;/code&gt; CTE — compute &lt;code&gt;new_session = 1&lt;/code&gt; when the gap to the previous event exceeds 30 minutes, or when it's the first event of the user. The &lt;code&gt;IS NULL&lt;/code&gt; branch handles the first event.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sessionized&lt;/code&gt; CTE — cumulative-sum the flag per user. Session 0 covers the events before the first gap; session 1 covers the next block; etc.&lt;/li&gt;
&lt;li&gt;Final SELECT — group by (user_id, session_id) and aggregate. &lt;code&gt;MIN(event_time)&lt;/code&gt; is the session start; &lt;code&gt;MAX(event_time)&lt;/code&gt; the end. &lt;code&gt;MIN(page_url) FILTER (WHERE event_name = 'page_view')&lt;/code&gt; picks the first pageview URL as the landing page.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;FILTER&lt;/code&gt; clause is ANSI standard (Postgres, Snowflake, BigQuery); SQL Server uses &lt;code&gt;MIN(CASE WHEN event_name = 'page_view' THEN page_url END)&lt;/code&gt; as the equivalent. Same effect.&lt;/li&gt;
&lt;li&gt;Extend with more per-session aggregates as needed — total revenue, exit page, campaign source, etc. The pattern generalises without re-sessionizing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;session_id&lt;/th&gt;
&lt;th&gt;session_start&lt;/th&gt;
&lt;th&gt;session_end&lt;/th&gt;
&lt;th&gt;session_duration&lt;/th&gt;
&lt;th&gt;event_count&lt;/th&gt;
&lt;th&gt;landing_page&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;2026-07-01 10:00&lt;/td&gt;
&lt;td&gt;2026-07-01 10:20&lt;/td&gt;
&lt;td&gt;00:20:00&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;/home&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2026-07-01 11:15&lt;/td&gt;
&lt;td&gt;2026-07-01 11:20&lt;/td&gt;
&lt;td&gt;00:05:00&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;/home&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Sessionization is one CTE for the flag, one CTE for the id, one SELECT for the aggregation. Three tiny queries; one whole pipeline.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — configurable-threshold sessionization macro
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; In a real analytics stack, the threshold is a parameter — 30 min for consumer apps, 60 min for long-attention apps, 10 min for high-frequency apps. A dbt macro takes the threshold as an argument and generates the correct SQL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write a dbt macro &lt;code&gt;sessionize(events_relation, user_col, ts_col, threshold_minutes=30)&lt;/code&gt; that emits sessionization SQL parameterised on the threshold. Show the macro and one invocation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt; (Same as above — user_id, event_time, event_name, page_url.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- macros/sessionize.sql&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;macro&lt;/span&gt; &lt;span class="n"&gt;sessionize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;events_relation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_col&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'user_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts_col&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'event_time'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;threshold_minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;flagged&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
      &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="k"&gt;CASE&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;ts_col&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;({{&lt;/span&gt; &lt;span class="n"&gt;ts_col&lt;/span&gt; &lt;span class="p"&gt;}})&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;user_col&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;ts_col&lt;/span&gt; &lt;span class="p"&gt;}})&lt;/span&gt;
             &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'{{ threshold_minutes }} minutes'&lt;/span&gt;
          &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;({{&lt;/span&gt; &lt;span class="n"&gt;ts_col&lt;/span&gt; &lt;span class="p"&gt;}})&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;user_col&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;ts_col&lt;/span&gt; &lt;span class="p"&gt;}})&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
          &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
      &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;new_session&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;events_relation&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;user_col&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;ts_col&lt;/span&gt; &lt;span class="p"&gt;}})&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;flagged&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;endmacro&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;-- usage in a model&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;sessionized&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;sessionize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'events'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'user_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'event_time'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;session_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;session_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_count&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sessionized&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The macro accepts the events relation, the user column name, the timestamp column name, and the threshold in minutes. Defaults match the Segment convention.&lt;/li&gt;
&lt;li&gt;Inside the macro, the sessionization primitive is the same LAG + CASE + SUM OVER pattern. The threshold is templated into the &lt;code&gt;INTERVAL '{{ threshold_minutes }} minutes'&lt;/code&gt; clause.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SELECT *&lt;/code&gt; inside the CTE forwards every downstream column — event_name, page_url, revenue, etc. — through the sessionization pass. Callers don't have to enumerate columns.&lt;/li&gt;
&lt;li&gt;Usage — call &lt;code&gt;{{ sessionize(ref('events'), ...) }}&lt;/code&gt; inside a model. The compiled SQL is a plain CTE that any warehouse can execute.&lt;/li&gt;
&lt;li&gt;Adapting for BigQuery — replace &lt;code&gt;INTERVAL '{{ threshold_minutes }} minutes'&lt;/code&gt; with &lt;code&gt;INTERVAL {{ threshold_minutes }} MINUTE&lt;/code&gt;. Add a dispatch on &lt;code&gt;target.type&lt;/code&gt; to pick the right syntax for every warehouse.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt; (Same as the raw sessionization query — one row per (user, session) pair.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Parameterise the threshold from day one. Hard-coding 30 minutes leads to code duplication when a new app onboards with a different threshold. A macro pays for itself the first time you need a second value.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — session close on explicit sign-out
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Some products close sessions on explicit user actions — sign-out, tab-close, "end conversation." The pattern extends the boundary flag to include the explicit close: any row where &lt;code&gt;event_name = 'sign_out'&lt;/code&gt; starts a new session on the &lt;em&gt;next&lt;/em&gt; event.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given &lt;code&gt;events(user_id, event_time, event_name)&lt;/code&gt; where &lt;code&gt;event_name = 'sign_out'&lt;/code&gt; explicitly ends a session, write sessionization SQL that respects both the 30-min inactivity rule and the sign-out event.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;event_time&lt;/th&gt;
&lt;th&gt;event_name&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;10:00&lt;/td&gt;
&lt;td&gt;page_view&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;10:05&lt;/td&gt;
&lt;td&gt;sign_out&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;10:07&lt;/td&gt;
&lt;td&gt;page_view&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;10:10&lt;/td&gt;
&lt;td&gt;click&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;flagged&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CASE&lt;/span&gt;
      &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'30 minutes'&lt;/span&gt;
        &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
        &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'sign_out'&lt;/span&gt;
        &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;new_session&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;event_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;flagged&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The CASE now has three branches — inactivity gap, first event, and "previous event was sign_out." Any of them flags a new session.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LAG(event_name)&lt;/code&gt; looks at the previous event's name. If it was &lt;code&gt;sign_out&lt;/code&gt;, the current event starts a new session — even if the gap to the previous is 2 minutes.&lt;/li&gt;
&lt;li&gt;Note that the &lt;code&gt;sign_out&lt;/code&gt; event itself belongs to the &lt;em&gt;ending&lt;/em&gt; session, not the new one. It's the &lt;em&gt;next&lt;/em&gt; event after sign_out that opens a new session.&lt;/li&gt;
&lt;li&gt;If the product wants "sign_out ends the session and the next event opens a new one immediately," this is correct. If it wants "sign_out itself is the last event of the ending session but the next event stays in the same session," drop the LAG branch.&lt;/li&gt;
&lt;li&gt;Generalise: any explicit boundary event (page_close, conversation_end, "start new task") can be modelled by adding a LAG check on the boundary event name.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;event_time&lt;/th&gt;
&lt;th&gt;event_name&lt;/th&gt;
&lt;th&gt;session_id&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;10:00&lt;/td&gt;
&lt;td&gt;page_view&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;10:05&lt;/td&gt;
&lt;td&gt;sign_out&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;10:07&lt;/td&gt;
&lt;td&gt;page_view&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;10:10&lt;/td&gt;
&lt;td&gt;click&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For any product with explicit session-close semantics, add a &lt;code&gt;LAG(event_name) = 'boundary_event'&lt;/code&gt; check to the new-session flag. The rest of the pipeline stays the same.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on session-level attribution
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Given a Segment event stream, sessionize with 30-min inactivity, and then attribute the last-touch campaign per session and per user. Walk me through the query and explain how you'd handle a user whose last-touch campaign came from a session 30 days ago."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using sessionize + last-touch join + campaign attribution
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;flagged&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;utm_campaign&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CASE&lt;/span&gt;
      &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'30 minutes'&lt;/span&gt;
        &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
        &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;new_session&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;sessionized&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;utm_campaign&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;flagged&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;per_session&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;session_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;session_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;-- Last touch = the last non-null utm_campaign in the session&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ARRAY_AGG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;utm_campaign&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;utm_campaign&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;))[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;last_touch_campaign&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sessionized&lt;/span&gt;
  &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;per_user_last_touch&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;last_touch_campaign&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;-- Fill the last-touch forward across sessions&lt;/span&gt;
    &lt;span class="n"&gt;LAST_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last_touch_campaign&lt;/span&gt; &lt;span class="k"&gt;IGNORE&lt;/span&gt; &lt;span class="n"&gt;NULLS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;
      &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;session_start&lt;/span&gt;
      &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="n"&gt;UNBOUNDED&lt;/span&gt; &lt;span class="k"&gt;PRECEDING&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;CURRENT&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;user_last_touch_campaign&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;session_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;session_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_count&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;per_session&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;per_user_last_touch&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_start&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;What runs&lt;/th&gt;
&lt;th&gt;Output shape&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Sessionize the event stream&lt;/td&gt;
&lt;td&gt;Same rows plus session_id&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Per-session aggregate&lt;/td&gt;
&lt;td&gt;One row per (user, session) with last_touch_campaign&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Forward-fill last-touch across sessions&lt;/td&gt;
&lt;td&gt;Every session sees its user's most recent non-null campaign&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Final SELECT&lt;/td&gt;
&lt;td&gt;Session-level output with both session and user attribution&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The three-step attribution pipeline sessionizes, aggregates to session-level, then forward-fills the last non-null campaign across a user's session history. The forward-fill is a &lt;code&gt;LAST_VALUE IGNORE NULLS&lt;/code&gt; — the same trick that GA's last-non-direct-click attribution uses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;session_id&lt;/th&gt;
&lt;th&gt;last_touch_campaign&lt;/th&gt;
&lt;th&gt;user_last_touch_campaign&lt;/th&gt;
&lt;th&gt;session_start&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;google_ads&lt;/td&gt;
&lt;td&gt;google_ads&lt;/td&gt;
&lt;td&gt;2026-07-01&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;google_ads&lt;/td&gt;
&lt;td&gt;2026-07-05&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;facebook_ads&lt;/td&gt;
&lt;td&gt;facebook_ads&lt;/td&gt;
&lt;td&gt;2026-07-10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Sessionize once, aggregate once, forward-fill once&lt;/strong&gt;&lt;/strong&gt; — the three-step pipeline mirrors the three-step gaps-and-islands universal template. Every extension (attribution, funnel, retention) reads the sessionized shape and adds one aggregation step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;FILTER + ARRAY_AGG for last non-null&lt;/strong&gt;&lt;/strong&gt; — the &lt;code&gt;ARRAY_AGG(... ORDER BY ... DESC) FILTER (WHERE ... IS NOT NULL)&lt;/code&gt; trick picks the most recent non-null value inside a session. Portable to Postgres, Snowflake, BigQuery. SQL Server uses &lt;code&gt;MAX(...) OVER (ORDER BY ...)&lt;/code&gt; with a case statement equivalent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;LAST_VALUE IGNORE NULLS for forward fill&lt;/strong&gt;&lt;/strong&gt; — the ANSI standard for "carry the last non-null value forward" — Postgres 16+, Snowflake, BigQuery, SQL Server 2022+ all ship it. The &lt;code&gt;ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW&lt;/code&gt; frame is required — without it, &lt;code&gt;LAST_VALUE&lt;/code&gt; reads the whole partition and returns the last row's value instead of the running last.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cross-session state without joins&lt;/strong&gt;&lt;/strong&gt; — the &lt;code&gt;LAST_VALUE OVER&lt;/code&gt; primitive replaces a self-join that would otherwise be O(N²). Window functions are the analytics engineer's go-to when a self-join is tempting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — three window passes plus one aggregate. On a 1B-row event table with 10M users, the sessionization pass is dominant (O(N log N)); the aggregation is O(N); the forward-fill is O(N). Total plan: a few window scans, no joins, very cache-friendly. Modern warehouses run this in single-digit minutes on billion-row inputs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — time-series&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Time-series analytics drills&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/time-series" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — interval processing&lt;/span&gt;
&lt;strong&gt;Interval processing problems&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/interval-processing" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  4. Consecutive-day streaks
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;sql streaks&lt;/code&gt; — the LeetCode-flavoured "longest consecutive-day streak per user" problem, plus current-streak framing and gap tolerance
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;a consecutive-day streak is a dense-date island where the ordered attribute is the login date and the aggregate is &lt;code&gt;COUNT(*)&lt;/code&gt;; the "longest streak" is the max per user and the "current streak" is the island whose end date is today&lt;/strong&gt;. Once you say "streaks = date islands + per-user max," the streak interview surface reduces to the standard Tabibitosan template plus one extra ranking step.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe7vqgq3bo6za8trtob41.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe7vqgq3bo6za8trtob41.jpeg" alt="Visual diagram of consecutive-day login streaks — left an input table of user login dates, a purple arrow labelled 'group by island_id', right a longest-streak and current-streak card per user; a small annotation about gap tolerance and holiday handling; on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The two streak questions.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Longest streak per user.&lt;/strong&gt; Given all-time logins, what's the longest consecutive-day run each user has ever had? Reported on user profiles ("your longest streak: 47 days").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Current streak per user.&lt;/strong&gt; Given today's date, is the user still on a streak, and if so, how long is it? Powered by the daily-open notification that says "keep your 47-day streak alive."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The three-step recipe.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Step 1 — dense-date island id&lt;/span&gt;
&lt;span class="n"&gt;island_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;login_date&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'1 day'&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 2 — per-island aggregates&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;island_id&lt;/span&gt;
  &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="n"&gt;island_start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;island_end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 3 — pick the longest or the current&lt;/span&gt;
&lt;span class="n"&gt;longest&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;length&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;current&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;island_end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;CURRENT_DATE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Gap tolerance — the "one-day miss" allowed variant.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sometimes a product allows a 1-day miss without breaking the streak — "you can miss one day per week." The recipe extends:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Flag "gap &amp;gt; tolerance" instead of "gap &amp;gt; 1"&lt;/span&gt;
&lt;span class="k"&gt;CASE&lt;/span&gt;
  &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;login_date&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'2 days'&lt;/span&gt;
    &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
    &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;new_streak&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Then cumulative-sum the flag to get an "extended island id," and aggregate as before. The threshold is the max allowed gap; 2 days tolerates a 1-day miss.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Holiday tolerance — the calendar-join variant.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pre-join a business-calendar table with &lt;code&gt;is_holiday&lt;/code&gt; and &lt;code&gt;is_active_day&lt;/code&gt; flags. Filter login candidates to only active days before applying the island trick. Or use the calendar's &lt;code&gt;active_day_num&lt;/code&gt; as the ordered attribute (as in Section 2's business-day example).&lt;/li&gt;
&lt;li&gt;For "streak counts if the user is active on every non-holiday" — join the calendar into &lt;code&gt;logins&lt;/code&gt; and check that every active day is present.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Current streak — the "streak is still alive today" check.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;After computing per-island aggregates, filter to &lt;code&gt;island_end IN (CURRENT_DATE, CURRENT_DATE - INTERVAL '1 day')&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Including yesterday accounts for timezone edge cases — some users' "today" is still yesterday in UTC.&lt;/li&gt;
&lt;li&gt;The user has an active streak if the filtered row exists; the streak length is the island's length.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Streak-end timestamp for retention analysis.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Growth teams want to know &lt;em&gt;when&lt;/em&gt; users' streaks ended, not just their length. &lt;code&gt;island_end&lt;/code&gt; is exactly this.&lt;/li&gt;
&lt;li&gt;For churn cohort analysis, group users by &lt;code&gt;island_end&lt;/code&gt; week: "how many users ended a streak in week N?" is a leading indicator of churn.&lt;/li&gt;
&lt;li&gt;For "days since last streak," subtract &lt;code&gt;island_end&lt;/code&gt; from &lt;code&gt;CURRENT_DATE&lt;/code&gt;. Common in dashboards.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common &lt;code&gt;sql streaks&lt;/code&gt; interview probes.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"What's the difference between streak and session?"&lt;/strong&gt; — streaks are typically per-day granularity; sessions are per-minute. Same island-id trick, different ordered attribute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"How do you tolerate a 1-day miss?"&lt;/strong&gt; — extend the boundary threshold to 2 days (or use the flag-and-sum flavour with &lt;code&gt;gap &amp;gt; tolerance&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"How do you handle holidays?"&lt;/strong&gt; — pre-join a business calendar; use &lt;code&gt;working_day_num&lt;/code&gt; as the ordered attribute instead of raw dates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Current streak vs longest streak — same query?"&lt;/strong&gt; — same island-id primitive; different filter/rank at the end. Current filters to &lt;code&gt;island_end &amp;gt;= CURRENT_DATE - 1&lt;/code&gt;; longest picks the max per user.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Streaks across multiple activity types?"&lt;/strong&gt; — union the activities into a single "was_active_today" fact table, then apply the streak recipe.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Worked example — longest streak per user
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The canonical LeetCode-style ask: given a &lt;code&gt;logins(user_id, login_date)&lt;/code&gt; table with distinct rows per (user_id, login_date), return each user's longest consecutive-day streak with start, end, and length.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given &lt;code&gt;logins(user_id, login_date)&lt;/code&gt; distinct per (user, date), write a query that returns &lt;code&gt;(user_id, longest_streak_start, longest_streak_end, longest_streak_length)&lt;/code&gt; per user.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;login_date&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-01&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-02&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-03&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-06&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-07&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;2026-07-05&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;2026-07-06&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;2026-07-07&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;2026-07-08&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;2026-07-09&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;numbered&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;logins&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;islanded&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;login_date&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rn&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;' days'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;island_id&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;numbered&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;per_island&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;island_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;streak_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;streak_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;streak_length&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;islanded&lt;/span&gt;
  &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;island_id&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;ranked&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;streak_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;streak_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;streak_length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;
      &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;streak_length&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;streak_end&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;per_island&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;streak_start&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;longest_streak_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;streak_end&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;longest_streak_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;streak_length&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;longest_streak_length&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ranked&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;numbered&lt;/code&gt; CTE — per user, order by login_date and assign &lt;code&gt;rn&lt;/code&gt;. Same primitive as Section 2.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;islanded&lt;/code&gt; CTE — compute &lt;code&gt;island_id = login_date - rn days&lt;/code&gt;. Consecutive logins share the same island_id.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;per_island&lt;/code&gt; CTE — group by &lt;code&gt;(user_id, island_id)&lt;/code&gt; and aggregate to streak start/end/length.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ranked&lt;/code&gt; CTE — per user, rank the islands by length descending, tie-break on end date. &lt;code&gt;ROW_NUMBER() = 1&lt;/code&gt; picks the longest streak.&lt;/li&gt;
&lt;li&gt;Final SELECT — filter to the winners and rename for clarity. Output is one row per user.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;longest_streak_start&lt;/th&gt;
&lt;th&gt;longest_streak_end&lt;/th&gt;
&lt;th&gt;longest_streak_length&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-01&lt;/td&gt;
&lt;td&gt;2026-07-03&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;2026-07-05&lt;/td&gt;
&lt;td&gt;2026-07-09&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Longest streak = island-id trick + &lt;code&gt;ROW_NUMBER() = 1&lt;/code&gt; per user. Four CTEs, one output row per user. This is the answer any LeetCode "consecutive numbers" streak problem wants.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — current streak per user
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The "streak is still alive today" variant: filter the per-island aggregates to those whose &lt;code&gt;island_end&lt;/code&gt; is today or yesterday (accounting for timezone). Users without such an island have no current streak.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given the same &lt;code&gt;logins&lt;/code&gt; table and today's date, return every user's current streak (start, end, length). Users with no current streak should not appear in the output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;login_date&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-08&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-09&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;2026-07-05&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;2026-07-06&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u3&lt;/td&gt;
&lt;td&gt;2026-07-10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Assume today is 2026-07-10.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;numbered&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;logins&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;islanded&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;login_date&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rn&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;' days'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;island_id&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;numbered&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;per_island&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;island_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;streak_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;streak_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;streak_length&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;islanded&lt;/span&gt;
  &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;island_id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;streak_start&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;current_streak_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;streak_end&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;current_streak_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;streak_length&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;current_streak_length&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;per_island&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;streak_end&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;CURRENT_DATE&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'1 day'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;First three CTEs are identical to the "longest streak" query — compute per-island aggregates.&lt;/li&gt;
&lt;li&gt;The filter &lt;code&gt;WHERE streak_end &amp;gt;= CURRENT_DATE - INTERVAL '1 day'&lt;/code&gt; picks islands that ended today or yesterday. Yesterday accounts for timezone edge cases where a user's "today" may still be yesterday in UTC.&lt;/li&gt;
&lt;li&gt;Users who haven't logged in recently have no matching island — they drop out of the output.&lt;/li&gt;
&lt;li&gt;If a user has multiple islands ending in the window (unusual, but possible if the data is weird), the query returns both. In practice, a user has at most one current streak; add a &lt;code&gt;ROW_NUMBER = 1&lt;/code&gt; ranking step if you want to be defensive.&lt;/li&gt;
&lt;li&gt;For "current streak = 1 day" (user logged in only today), the same query returns a length-1 island. That's usually the right answer for streak-based retention.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;current_streak_start&lt;/th&gt;
&lt;th&gt;current_streak_end&lt;/th&gt;
&lt;th&gt;current_streak_length&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-08&lt;/td&gt;
&lt;td&gt;2026-07-10&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u3&lt;/td&gt;
&lt;td&gt;2026-07-10&lt;/td&gt;
&lt;td&gt;2026-07-10&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Current streak = same island primitive + filter to &lt;code&gt;island_end &amp;gt;= CURRENT_DATE - 1&lt;/code&gt;. No extra ranking needed — the filter alone selects the user's active streak.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — streak with 1-day-miss tolerance
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A common product ask: "let users miss one day without breaking the streak." The recipe swaps the dense-date subtraction for the flag-and-sum flavour with a 2-day boundary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given &lt;code&gt;logins(user_id, login_date)&lt;/code&gt;, allow up to a 1-day miss between consecutive logins and return the longest tolerant-streak per user.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;login_date&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-01&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-02&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-04&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-05&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-09&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;flagged&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CASE&lt;/span&gt;
      &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;login_date&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'2 days'&lt;/span&gt;
        &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
        &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;new_streak&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;logins&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;streaked&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_streak&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;streak_id&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;flagged&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;per_streak&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;streak_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;streak_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;streak_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;login_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;login_date&lt;/span&gt;&lt;span class="p"&gt;))::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;calendar_span&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;streaked&lt;/span&gt;
  &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;streak_id&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;ranked&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;streak_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;streak_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;login_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;calendar_span&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;
      &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;calendar_span&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;per_streak&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;streak_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;streak_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;login_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;calendar_span&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ranked&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Because we're allowing 1-day misses, the dense-date subtraction trick no longer works (a 1-day miss changes the &lt;code&gt;date - rn&lt;/code&gt; value). Switch to the flag-and-sum flavour.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;flagged&lt;/code&gt; CTE — flag &lt;code&gt;new_streak = 1&lt;/code&gt; when the gap to the previous login exceeds 2 days. A 1-day gap (miss one day) stays inside the streak.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;streaked&lt;/code&gt; CTE — cumulative-sum the flag per user to produce the tolerant streak_id.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;per_streak&lt;/code&gt; CTE — aggregate to &lt;code&gt;login_count&lt;/code&gt; (number of actual logins in the streak) and &lt;code&gt;calendar_span&lt;/code&gt; (span from first to last day, inclusive). The two can differ — a tolerant streak of 10 calendar days may only have 8 logins.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ranked&lt;/code&gt; CTE — pick the streak with the longest &lt;code&gt;calendar_span&lt;/code&gt; per user. If you'd rather rank by &lt;code&gt;login_count&lt;/code&gt;, swap the ORDER BY.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;streak_start&lt;/th&gt;
&lt;th&gt;streak_end&lt;/th&gt;
&lt;th&gt;login_count&lt;/th&gt;
&lt;th&gt;calendar_span&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026-07-01&lt;/td&gt;
&lt;td&gt;2026-07-05&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Tolerant streaks use the flag-and-sum flavour with a gap threshold. Reporting both &lt;code&gt;login_count&lt;/code&gt; and &lt;code&gt;calendar_span&lt;/code&gt; gives product a choice of streak definition without re-running the pipeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on streak analytics at scale
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "You run a language-learning app with 50M users and 10B daily-activity rows. Design a nightly pipeline that publishes each user's &lt;code&gt;(longest_streak, current_streak, days_since_last_streak_end)&lt;/code&gt; so it's ready for the daily push notification. Where do the joins happen and what does the plan cost?"&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a two-stage nightly pipeline with materialised streaks
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- STAGE 1 (nightly) — materialise per-user streaks table&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;user_streaks&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;numbered&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;activity_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;activity_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;daily_activity&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;islanded&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;activity_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;activity_date&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rn&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;' days'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;island_id&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;numbered&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;per_island&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;island_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;activity_date&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;streak_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;activity_date&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;streak_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;streak_length&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;islanded&lt;/span&gt;
  &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;island_id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;per_island&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- STAGE 2 (each morning) — build the push-notification-ready table&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;user_streak_summary&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;by_user&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;streak_length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;longest_streak&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;streak_end&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;CURRENT_DATE&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'1 day'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;streak_length&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;current_streak&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;streak_end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;last_streak_end&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;user_streaks&lt;/span&gt;
  &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;longest_streak&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_streak&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;current_streak&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CURRENT_DATE&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;last_streak_end&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;days_since_last_streak_end&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;by_user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;What runs&lt;/th&gt;
&lt;th&gt;Approx cost on 10B rows&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Compute per-user islands (window scan + GROUP BY)&lt;/td&gt;
&lt;td&gt;~90% of pipeline cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Aggregate to per-user summary&lt;/td&gt;
&lt;td&gt;~5% of pipeline cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2b&lt;/td&gt;
&lt;td&gt;Join summary to push service via user_id&lt;/td&gt;
&lt;td&gt;~5% for the export step&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Stage 1 is where the heavy lifting happens; it's a nightly job and gets 30 minutes to run on a warehouse cluster. Stage 2 is cheap and can run in seconds after Stage 1 lands. The push service reads Stage 2 by user_id at delivery time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;longest_streak&lt;/th&gt;
&lt;th&gt;current_streak&lt;/th&gt;
&lt;th&gt;days_since_last_streak_end&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;47&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u3&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Split the pipeline into materialise + summarise&lt;/strong&gt;&lt;/strong&gt; — the expensive island detection runs nightly and lands in a table; the cheap per-user aggregation runs each morning and lands in the push-service-ready table. This split avoids re-computing islands every morning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Idempotent materialisation&lt;/strong&gt;&lt;/strong&gt; — the &lt;code&gt;user_streaks&lt;/code&gt; table can be rebuilt on any night without breaking downstream consumers; the island ids are deterministic given the input data. If the pipeline fails, you re-run and get the same output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;COALESCE for the "no current streak" case&lt;/strong&gt;&lt;/strong&gt; — the &lt;code&gt;MAX(CASE WHEN ...)&lt;/code&gt; returns NULL for users without a current streak; &lt;code&gt;COALESCE(current_streak, 0)&lt;/code&gt; fills in zero. The push notification templates check &lt;code&gt;current_streak &amp;gt; 0&lt;/code&gt; to decide whether to send.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;days_since_last_streak_end for retention&lt;/strong&gt;&lt;/strong&gt; — the delta from today to the user's last streak end is a leading indicator of churn. Growth teams filter to &lt;code&gt;days_since_last_streak_end BETWEEN 3 AND 7&lt;/code&gt; and target re-engagement campaigns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — Stage 1 is O(N log N) per user for the window sort. On 10B rows with 50M users, this is ~1B operations per partition summed over partitions — 15-30 minutes on a well-clustered Snowflake medium warehouse. Stage 2 is O(unique users) — seconds. Overall, the pipeline hits the daily push cutoff comfortably.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — window functions (hard)&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Window function problems (SQL, hard)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/window-functions/sql/hard" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — sliding window&lt;/span&gt;
&lt;strong&gt;Sliding window drills&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sliding-window" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  5. Run-length encoding + dialect matrix
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;sql run length encoding&lt;/code&gt; compresses a state series into runs — the &lt;code&gt;SUM(CASE WHEN prev &amp;lt;&amp;gt; curr) OVER&lt;/code&gt; primitive, plus the MATCH_RECOGNIZE alternative for Snowflake and Oracle
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;RLE compresses a sequence of state values into one row per contiguous run using the same LAG + SUM-of-change-flag primitive as sessionization; when the warehouse ships MATCH_RECOGNIZE (Snowflake, Oracle), you get a declarative alternative that reads like a regex over rows&lt;/strong&gt;. Once you say "RLE = flag change points + cumulative sum, or MATCH_RECOGNIZE PATTERN," the RLE interview surface reduces to a dialect lookup.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fel99zqj1w240ofjac8v1.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fel99zqj1w240ofjac8v1.jpeg" alt="Visual diagram of SQL run-length encoding and the MATCH_RECOGNIZE dialect matrix — top an RLE example with a status series and the compressed run output, bottom a 4-column dialect card comparing Postgres, Snowflake, BigQuery, and SQL Server support for MATCH_RECOGNIZE; on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The RLE primitive.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Given (t, status) ordered by t, compress consecutive same-status rows into runs&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;flagged&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CASE&lt;/span&gt;
      &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
        &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;is_new_run&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;series&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;grouped&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;is_new_run&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;flagged&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_length&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;grouped&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;run_start&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three CTEs, one output row per run. This is the portable answer that works on every warehouse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where RLE lands in pipelines.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Status series compression.&lt;/strong&gt; Sensor readings, service uptime, feature-flag states — sequences with many consecutive same-value rows. Storage cost drops 10-100×.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price plateaus.&lt;/strong&gt; Slow-moving prices (real-estate listings, insurance quotes) compress dramatically. Financial modelling often carries prices as runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feature-flag audit logs.&lt;/strong&gt; "This flag was &lt;code&gt;on&lt;/code&gt; for user X from t1 to t2, then &lt;code&gt;off&lt;/code&gt; from t2 to t3." Downstream A/B analysis reads this shape.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slowly changing dimensions (SCD-2).&lt;/strong&gt; Type-2 slowly-changing dimensions store one row per contiguous state — that's RLE on the SCD-2 output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CDC compaction.&lt;/strong&gt; Debezium and Fivetran ship one row per change event; downstream compaction collapses consecutive unchanged rows into RLE runs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;MATCH_RECOGNIZE — the declarative alternative.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Snowflake / Oracle syntax&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;series&lt;/span&gt;
&lt;span class="n"&gt;MATCH_RECOGNIZE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
  &lt;span class="n"&gt;MEASURES&lt;/span&gt;
    &lt;span class="k"&gt;FIRST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;LAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;
  &lt;span class="n"&gt;ONE&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="n"&gt;PER&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt;
  &lt;span class="n"&gt;PATTERN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt; &lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;DEFINE&lt;/span&gt;
    &lt;span class="n"&gt;A&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;B&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Reads like a regex over the row sequence: match one row &lt;code&gt;A&lt;/code&gt;, then zero or more rows &lt;code&gt;B&lt;/code&gt; where &lt;code&gt;B.status = A.status&lt;/code&gt;. Each match emits one output row.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MEASURES&lt;/code&gt; block declares the per-match columns.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ONE ROW PER MATCH&lt;/code&gt; — one row per run. &lt;code&gt;ALL ROWS PER MATCH&lt;/code&gt; would preserve the raw rows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;MATCH_RECOGNIZE dialect matrix.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Warehouse&lt;/th&gt;
&lt;th&gt;MATCH_RECOGNIZE support&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Oracle 12c+&lt;/td&gt;
&lt;td&gt;YES — full ANSI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snowflake&lt;/td&gt;
&lt;td&gt;YES — full ANSI (since 2021)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Postgres&lt;/td&gt;
&lt;td&gt;NO (as of 2026) — use LAG + SUM(new_group) OVER&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BigQuery&lt;/td&gt;
&lt;td&gt;NO — use LAG + SUM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL Server&lt;/td&gt;
&lt;td&gt;NO — use LAG + SUM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL 8&lt;/td&gt;
&lt;td&gt;NO — use LAG + SUM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redshift&lt;/td&gt;
&lt;td&gt;NO — use LAG + SUM&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Choosing between the two.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Portable code.&lt;/strong&gt; Use LAG + SUM OVER. Works on every warehouse; readable; easy to reason about.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complex multi-row patterns.&lt;/strong&gt; Use MATCH_RECOGNIZE. Patterns like "A then B+ then C" (one A, one or more B, one C) are trivial in MATCH_RECOGNIZE and painful in window functions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regex-like row patterns.&lt;/strong&gt; Use MATCH_RECOGNIZE. Financial fraud detection, sequence pattern mining, click-stream funnel matching are the classic use cases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simple RLE.&lt;/strong&gt; Either works. Prefer LAG + SUM for portability.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Postgres-specific extension: &lt;code&gt;RANGE INCLUSIVE&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Postgres ships a range-type extension that can store &lt;code&gt;(status, tsrange)&lt;/code&gt; directly. &lt;code&gt;range_agg(tsrange(t, t + INTERVAL '1 day'))&lt;/code&gt; collapses consecutive same-status rows into a range set.&lt;/li&gt;
&lt;li&gt;Concise for storage but non-portable — only Postgres and a few others support ranges.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Snowflake-specific extension: &lt;code&gt;AUTOMATIC_CLUSTERING&lt;/code&gt; on the RLE output.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Materialise the RLE output as a table and cluster on the state column. Queries like "get all runs where status = 'error' in the last month" become micro-partition-pruned.&lt;/li&gt;
&lt;li&gt;Common in reliability dashboards where 99% of runs are &lt;code&gt;ok&lt;/code&gt; and 1% are &lt;code&gt;error&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common &lt;code&gt;sql run length encoding&lt;/code&gt; interview probes.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Explain the SUM(CASE WHEN prev &amp;lt;&amp;gt; curr) trick."&lt;/strong&gt; — LAG produces the previous value; CASE flags the change point with a 1; SUM OVER accumulates the flags into a run id.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Handle the first row where LAG is NULL."&lt;/strong&gt; — extra branch in the CASE: &lt;code&gt;OR LAG(...) IS NULL THEN 1&lt;/code&gt;. The first row always starts a run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Which warehouses support MATCH_RECOGNIZE?"&lt;/strong&gt; — Oracle 12c+, Snowflake. Not Postgres, BigQuery, SQL Server, MySQL, Redshift.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"What's the compression ratio?"&lt;/strong&gt; — depends on the value stability. A series where 90% of rows are duplicates compresses ~10×. Slowly-changing prices compress 50-100×.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"How do you handle nulls in the state column?"&lt;/strong&gt; — treat NULL as its own value (&lt;code&gt;IS NOT DISTINCT FROM&lt;/code&gt; comparison) or exclude NULLs entirely. Every downstream consumer's answer differs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Worked example — feature-flag rollout compression
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A feature-flag audit table logs one row per second per user showing the flag state. Most seconds have the same state as the previous second. RLE compresses to one row per (user, state, interval), cutting storage by 10-100×.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given &lt;code&gt;flag_log(user_id, log_time, flag_state)&lt;/code&gt; with per-second rows, compress to one row per (user_id, flag_state, run) with run start / end / length.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;log_time&lt;/th&gt;
&lt;th&gt;flag_state&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;10:00:01&lt;/td&gt;
&lt;td&gt;off&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;10:00:02&lt;/td&gt;
&lt;td&gt;off&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;10:00:03&lt;/td&gt;
&lt;td&gt;on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;10:00:04&lt;/td&gt;
&lt;td&gt;on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;10:00:05&lt;/td&gt;
&lt;td&gt;on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;10:00:06&lt;/td&gt;
&lt;td&gt;off&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;flagged&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;log_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;flag_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CASE&lt;/span&gt;
      &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;flag_state&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flag_state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;log_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flag_state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;log_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
        &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;is_new_run&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;flag_log&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;grouped&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;log_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;flag_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;is_new_run&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;log_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;flagged&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;flag_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;log_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;log_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_length_seconds&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;grouped&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flag_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;run_start&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;flagged&lt;/code&gt; CTE — flag &lt;code&gt;is_new_run = 1&lt;/code&gt; when the current &lt;code&gt;flag_state&lt;/code&gt; differs from the previous row's, or when there's no previous row (first row per user). The &lt;code&gt;PARTITION BY user_id&lt;/code&gt; scoped LAG ensures per-user comparison.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;grouped&lt;/code&gt; CTE — cumulative-sum &lt;code&gt;is_new_run&lt;/code&gt; per user to produce &lt;code&gt;run_id&lt;/code&gt;. Every 1 increments the id; every 0 preserves it.&lt;/li&gt;
&lt;li&gt;Outer SELECT — group by &lt;code&gt;(user_id, flag_state, run_id)&lt;/code&gt;. Including &lt;code&gt;flag_state&lt;/code&gt; in the group is defensive; the run_id already implies the state, but including it keeps the query readable and avoids surprises if the CASE ever fires incorrectly.&lt;/li&gt;
&lt;li&gt;Aggregations — &lt;code&gt;MIN(log_time)&lt;/code&gt; is the run start, &lt;code&gt;MAX(log_time)&lt;/code&gt; the end, &lt;code&gt;COUNT(*)&lt;/code&gt; the run length in seconds. For minute-granularity, divide &lt;code&gt;COUNT(*)&lt;/code&gt; by 60.&lt;/li&gt;
&lt;li&gt;Compression ratio — the input has one row per second per user; the output has one row per state change per user. If the average state duration is 10 minutes, compression is ~600× per user.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;flag_state&lt;/th&gt;
&lt;th&gt;run_start&lt;/th&gt;
&lt;th&gt;run_end&lt;/th&gt;
&lt;th&gt;run_length_seconds&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;off&lt;/td&gt;
&lt;td&gt;10:00:01&lt;/td&gt;
&lt;td&gt;10:00:02&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;on&lt;/td&gt;
&lt;td&gt;10:00:03&lt;/td&gt;
&lt;td&gt;10:00:05&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;off&lt;/td&gt;
&lt;td&gt;10:00:06&lt;/td&gt;
&lt;td&gt;10:00:06&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Every slowly-changing state series is a RLE candidate. Compression ratio equals the average run length; storage cost drops proportionally. Materialise the compressed shape as a downstream table for cheap analytical reads.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — MATCH_RECOGNIZE on Snowflake
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Snowflake ships full ANSI MATCH_RECOGNIZE. The RLE query above compresses to a shorter, more readable form using PATTERN and DEFINE clauses. This is the Snowflake-native answer to the same question.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; On Snowflake, write the same feature-flag RLE query using MATCH_RECOGNIZE.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt; (Same as above.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;flag_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;run_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;run_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;run_length&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;flag_log&lt;/span&gt;
&lt;span class="n"&gt;MATCH_RECOGNIZE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;log_time&lt;/span&gt;
  &lt;span class="n"&gt;MEASURES&lt;/span&gt;
    &lt;span class="k"&gt;FIRST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;log_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;LAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;log_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flag_state&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;flag_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_length&lt;/span&gt;
  &lt;span class="n"&gt;ONE&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="n"&gt;PER&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt;
  &lt;span class="n"&gt;PATTERN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt; &lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;DEFINE&lt;/span&gt;
    &lt;span class="n"&gt;A&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;B&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flag_state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flag_state&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;PARTITION BY user_id&lt;/code&gt; — sessionize each user independently, same as the window functions do.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ORDER BY log_time&lt;/code&gt; — establish the row order to match against.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PATTERN (A B*)&lt;/code&gt; — match one &lt;code&gt;A&lt;/code&gt; row, then zero or more &lt;code&gt;B&lt;/code&gt; rows. Every run starts with an &lt;code&gt;A&lt;/code&gt; and continues with &lt;code&gt;B&lt;/code&gt;s.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DEFINE A AS TRUE, B AS B.flag_state = A.flag_state&lt;/code&gt; — &lt;code&gt;A&lt;/code&gt; matches every row unconditionally; &lt;code&gt;B&lt;/code&gt; matches the next row only if its &lt;code&gt;flag_state&lt;/code&gt; equals &lt;code&gt;A.flag_state&lt;/code&gt; — i.e., the run continues while the state is unchanged.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MEASURES&lt;/code&gt; — declare the columns for each match. &lt;code&gt;FIRST(log_time)&lt;/code&gt; is the run start; &lt;code&gt;LAST(log_time)&lt;/code&gt; the end; &lt;code&gt;A.flag_state&lt;/code&gt; the state; &lt;code&gt;COUNT(*)&lt;/code&gt; the length.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt; (Same as the LAG + SUM query.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; MATCH_RECOGNIZE is the Snowflake-native RLE syntax; the LAG + SUM version is portable. For a Snowflake-only codebase, MATCH_RECOGNIZE reads more clearly. For a multi-warehouse dbt project, stick with LAG + SUM.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — price plateau compression across dialects
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A price series with slow changes compresses dramatically under RLE. This example writes the same query in the two flavours (portable and Snowflake) so a candidate can compare readability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given &lt;code&gt;prices(symbol, ts, price)&lt;/code&gt; with per-minute rows and mostly-stable prices, compress to one row per (symbol, price, run) with run start / end / length. Show both the portable and Snowflake versions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;symbol&lt;/th&gt;
&lt;th&gt;ts&lt;/th&gt;
&lt;th&gt;price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AAPL&lt;/td&gt;
&lt;td&gt;09:30&lt;/td&gt;
&lt;td&gt;190&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AAPL&lt;/td&gt;
&lt;td&gt;09:31&lt;/td&gt;
&lt;td&gt;190&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AAPL&lt;/td&gt;
&lt;td&gt;09:32&lt;/td&gt;
&lt;td&gt;191&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AAPL&lt;/td&gt;
&lt;td&gt;09:33&lt;/td&gt;
&lt;td&gt;191&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AAPL&lt;/td&gt;
&lt;td&gt;09:34&lt;/td&gt;
&lt;td&gt;191&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AAPL&lt;/td&gt;
&lt;td&gt;09:35&lt;/td&gt;
&lt;td&gt;190&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Portable — LAG + SUM (works on every warehouse)&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;flagged&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CASE&lt;/span&gt;
      &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
        &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;is_new_run&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;prices&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;grouped&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;is_new_run&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;flagged&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_length_minutes&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;grouped&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;run_start&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Snowflake — MATCH_RECOGNIZE (shorter, less portable)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;run_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;run_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;run_length_minutes&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;prices&lt;/span&gt;
&lt;span class="n"&gt;MATCH_RECOGNIZE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;
  &lt;span class="n"&gt;MEASURES&lt;/span&gt;
    &lt;span class="k"&gt;FIRST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;LAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_length_minutes&lt;/span&gt;
  &lt;span class="n"&gt;ONE&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="n"&gt;PER&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt;
  &lt;span class="n"&gt;PATTERN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt; &lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;DEFINE&lt;/span&gt;
    &lt;span class="n"&gt;A&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;B&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Both queries produce the same output. The portable version reads any warehouse; the MATCH_RECOGNIZE version reads Snowflake or Oracle only.&lt;/li&gt;
&lt;li&gt;The portable version has three CTEs and one GROUP BY. The MATCH_RECOGNIZE version has one query — shorter but requires familiarity with PATTERN syntax.&lt;/li&gt;
&lt;li&gt;Plan quality — on Snowflake, both compile to similar physical plans (a window scan or a MATCH_RECOGNIZE-specific operator). MATCH_RECOGNIZE has a slight edge for complex multi-row patterns; for simple RLE, the plans are indistinguishable.&lt;/li&gt;
&lt;li&gt;When to prefer MATCH_RECOGNIZE: patterns like "buy signal followed by three consecutive down-days followed by a stop-loss trigger" — regex-like. When to prefer LAG + SUM: simple RLE, sessionization, streaks — the classic patterns.&lt;/li&gt;
&lt;li&gt;Team fluency — MATCH_RECOGNIZE requires training. If your team knows window functions cold and MATCH_RECOGNIZE not at all, choose the portable version. Every hour of training is a real cost.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;symbol&lt;/th&gt;
&lt;th&gt;price&lt;/th&gt;
&lt;th&gt;run_start&lt;/th&gt;
&lt;th&gt;run_end&lt;/th&gt;
&lt;th&gt;run_length_minutes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AAPL&lt;/td&gt;
&lt;td&gt;190&lt;/td&gt;
&lt;td&gt;09:30&lt;/td&gt;
&lt;td&gt;09:31&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AAPL&lt;/td&gt;
&lt;td&gt;191&lt;/td&gt;
&lt;td&gt;09:32&lt;/td&gt;
&lt;td&gt;09:34&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AAPL&lt;/td&gt;
&lt;td&gt;190&lt;/td&gt;
&lt;td&gt;09:35&lt;/td&gt;
&lt;td&gt;09:35&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Write portable LAG + SUM for cross-warehouse code. Reach for MATCH_RECOGNIZE only on Snowflake or Oracle when the pattern is genuinely multi-row and regex-like.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on dialect-portable RLE
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Your team maintains an analytics warehouse on Postgres and a data-science warehouse on Snowflake. Design the SCD-2 compaction pipeline that runs identically on both and produces the same &lt;code&gt;(user_id, state, valid_from, valid_to)&lt;/code&gt; history — walk me through the code and why you'd not use MATCH_RECOGNIZE even though Snowflake supports it."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a portable LAG + SUM SCD-2 compactor
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- SCD-2 compactor — portable Postgres + Snowflake + BigQuery + SQL Server + MySQL&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;user_state_scd2&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;flagged&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;log_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CASE&lt;/span&gt;
      &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;state&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;log_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;log_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
        &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;is_new_run&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;user_state_log&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;grouped&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;log_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;is_new_run&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;log_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;flagged&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;log_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;valid_from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;log_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;valid_to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;log_time&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;scd_version&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;grouped&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;What runs&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;flagged CTE (LAG + CASE)&lt;/td&gt;
&lt;td&gt;Flag change points as 1, else 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;grouped CTE (SUM OVER)&lt;/td&gt;
&lt;td&gt;Cumulative-sum to produce run_id per user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Aggregate to valid_from / valid_to&lt;/td&gt;
&lt;td&gt;One row per contiguous state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;ROW_NUMBER() per user&lt;/td&gt;
&lt;td&gt;Assign scd_version for change-tracking&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The four steps produce a SCD-2-shaped history: one row per contiguous state per user, each row has &lt;code&gt;valid_from&lt;/code&gt;, &lt;code&gt;valid_to&lt;/code&gt;, and a monotonically-increasing &lt;code&gt;scd_version&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;state&lt;/th&gt;
&lt;th&gt;valid_from&lt;/th&gt;
&lt;th&gt;valid_to&lt;/th&gt;
&lt;th&gt;event_count&lt;/th&gt;
&lt;th&gt;scd_version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;trial&lt;/td&gt;
&lt;td&gt;2026-06-01&lt;/td&gt;
&lt;td&gt;2026-06-14&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;paid&lt;/td&gt;
&lt;td&gt;2026-06-15&lt;/td&gt;
&lt;td&gt;2026-06-30&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;churned&lt;/td&gt;
&lt;td&gt;2026-07-01&lt;/td&gt;
&lt;td&gt;2026-07-10&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Portable primitive&lt;/strong&gt;&lt;/strong&gt; — LAG, CASE, SUM OVER, and ROW_NUMBER are ANSI-standard window functions. Every warehouse from Postgres 8.4 onwards ships them. The exact same view definition compiles on Postgres, Snowflake, BigQuery, SQL Server 2012+, MySQL 8, and Redshift.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;No MATCH_RECOGNIZE even where supported&lt;/strong&gt;&lt;/strong&gt; — even though Snowflake supports MATCH_RECOGNIZE, using it would fork the code. The maintenance cost of two parallel implementations vastly exceeds the readability improvement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;scd_version via ROW_NUMBER&lt;/strong&gt;&lt;/strong&gt; — a monotonically-increasing per-user version number that downstream consumers use as a stable join key. Every dimensional model wants this column; not having it is an interview red flag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;valid_to is inclusive&lt;/strong&gt;&lt;/strong&gt; — the last state's &lt;code&gt;valid_to&lt;/code&gt; is &lt;code&gt;MAX(log_time)&lt;/code&gt;, not &lt;code&gt;NULL&lt;/code&gt; or &lt;code&gt;9999-12-31&lt;/code&gt;. Downstream consumers doing "what's the current state?" filter by &lt;code&gt;WHERE CURRENT_TIMESTAMP BETWEEN valid_from AND valid_to&lt;/code&gt;. Adjust to your team's convention.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — three window scans + one aggregate. On a 1B-row log, the pipeline runs in single-digit minutes on a modest cluster. Materialise as a table if downstream reads are frequent; keep as a view if you re-run on every batch. Both are valid; the choice depends on the read/refresh ratio.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — window functions (SQL)&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Window functions in SQL — full library&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/window-functions/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;


&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — SQL&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL problem library — 450+ DE-focused questions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;





&lt;h2&gt;
  
  
  Cheat sheet — Gaps &amp;amp; Islands recipe list
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Universal template.&lt;/strong&gt; Order rows within each partition. Assign an island id (Flavour A: dense subtraction; Flavour B: cumulative sum of change flag). Group by island id and aggregate. Every gaps-and-islands answer uses this exact three-step pattern.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dense-date island id (Flavour A).&lt;/strong&gt; &lt;code&gt;login_date - ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date) * INTERVAL '1 day'&lt;/code&gt;. Constant inside an island; changes across islands. Tabibitosan trick.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Change-flag island id (Flavour B).&lt;/strong&gt; &lt;code&gt;SUM(CASE WHEN gap &amp;gt; threshold THEN 1 ELSE 0 END) OVER (PARTITION BY user_id ORDER BY ts)&lt;/code&gt;. Increments at every boundary crossing. Works for sessionization, RLE, uptime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sessionization primitive.&lt;/strong&gt; &lt;code&gt;LAG(event_time)&lt;/code&gt; + &lt;code&gt;CASE WHEN gap &amp;gt; 30 min THEN 1 ELSE 0 END&lt;/code&gt; + &lt;code&gt;SUM(new_session) OVER (PARTITION BY user_id ORDER BY event_time)&lt;/code&gt;. Threshold defaults to 30 min per GA / Segment convention.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Longest streak per user.&lt;/strong&gt; Compute island_id, aggregate to per-island (start, end, length), rank per user by length desc, filter to &lt;code&gt;ROW_NUMBER() = 1&lt;/code&gt;. Four CTEs; one row per user.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Current streak per user.&lt;/strong&gt; Same island primitive plus filter &lt;code&gt;WHERE island_end &amp;gt;= CURRENT_DATE - INTERVAL '1 day'&lt;/code&gt;. Includes yesterday for timezone tolerance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gap-tolerant streak.&lt;/strong&gt; Switch to Flavour B with a boundary threshold &amp;gt; 1 day. Threshold of 2 days tolerates a 1-day miss.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Business-day streak.&lt;/strong&gt; Pre-join a &lt;code&gt;business_days(date, business_day_num)&lt;/code&gt; calendar table; use &lt;code&gt;business_day_num&lt;/code&gt; as the ordered attribute so weekends and holidays don't break the streak.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RLE compression.&lt;/strong&gt; &lt;code&gt;LAG(state)&lt;/code&gt; + &lt;code&gt;CASE WHEN state &amp;lt;&amp;gt; LAG(state) THEN 1 ELSE 0 END&lt;/code&gt; + &lt;code&gt;SUM(is_new_run) OVER&lt;/code&gt;. Three CTEs, one aggregate. Compression ratio equals the average run length.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MATCH_RECOGNIZE syntax.&lt;/strong&gt; &lt;code&gt;MATCH_RECOGNIZE (PARTITION BY ... ORDER BY ... MEASURES FIRST(...), LAST(...), COUNT(*) ... ONE ROW PER MATCH PATTERN (A B*) DEFINE A AS TRUE, B AS B.state = A.state)&lt;/code&gt;. Snowflake + Oracle only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MATCH_RECOGNIZE dialect matrix.&lt;/strong&gt; Oracle 12c+ YES · Snowflake YES · Postgres NO · BigQuery NO · SQL Server NO · MySQL NO · Redshift NO. Default to portable LAG + SUM on any multi-warehouse project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PARTITION BY user_id is non-negotiable.&lt;/strong&gt; Every window function in every gaps-and-islands query partitions by the entity — user, session, symbol, device. Skipping the partition mixes entities; the interviewer catches it every time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First-and-last per island.&lt;/strong&gt; Swap &lt;code&gt;GROUP BY + MIN/MAX&lt;/code&gt; for &lt;code&gt;MIN() OVER (PARTITION BY ..., island_id)&lt;/code&gt; window aggregates when you want to enrich each row with its island's boundaries without collapsing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Top-1 per group.&lt;/strong&gt; &lt;code&gt;ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY length DESC) = 1&lt;/code&gt; is the portable "top-1 per user" idiom. Snowflake / BigQuery ship &lt;code&gt;QUALIFY ROW_NUMBER() = 1&lt;/code&gt; shorthand; Postgres has &lt;code&gt;DISTINCT ON (user_id) ORDER BY length DESC&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;FILTER (WHERE ...)&lt;/code&gt;.&lt;/strong&gt; Standard SQL clause for conditional aggregation — &lt;code&gt;MIN(page_url) FILTER (WHERE event_name = 'page_view')&lt;/code&gt;. Postgres, Snowflake, BigQuery. SQL Server uses &lt;code&gt;MIN(CASE WHEN ... THEN ... END)&lt;/code&gt; as the equivalent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LAST_VALUE IGNORE NULLS&lt;/code&gt; forward-fill.&lt;/strong&gt; For "carry the last non-null value forward" — &lt;code&gt;LAST_VALUE(x IGNORE NULLS) OVER (ORDER BY t ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)&lt;/code&gt;. Postgres 16+, Snowflake, BigQuery, SQL Server 2022+.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SCD-2 compactor.&lt;/code&gt;&lt;/strong&gt; RLE view over a CDC / state log with per-entity &lt;code&gt;valid_from&lt;/code&gt; / &lt;code&gt;valid_to&lt;/code&gt; and a &lt;code&gt;scd_version&lt;/code&gt;. Portable across every warehouse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compression math.&lt;/strong&gt; For a slowly-changing series, RLE cuts storage proportionally to the average run length. A 90% duplicate series compresses ~10×; slow-changing prices compress 50-100×.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When to reach for MATCH_RECOGNIZE.&lt;/strong&gt; Multi-row patterns with strict ordering ("A then B+ then C"), regex-like row sequences, fraud detection, sequence pattern mining. Rare in day-to-day analytics; common in specialised finance / risk workloads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When to stick with LAG + SUM.&lt;/strong&gt; Portability, team fluency, simple RLE, sessionization, streaks. The default answer for 90% of gaps-and-islands questions on 90% of warehouses.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is the gaps and islands problem in SQL?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;sql gaps and islands&lt;/code&gt; is a family of query patterns that identify runs of consecutive rows sharing some ordering property — for example, consecutive-day login streaks, sessionization of event streams, uptime windows, and price-plateau compression. The canonical answer assigns each row an "island id" and groups by that id: (a) dense-date sequences use &lt;code&gt;login_date - ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date)&lt;/code&gt; to produce a constant per island; (b) computed-boundary sequences use &lt;code&gt;SUM(CASE WHEN gap &amp;gt; threshold THEN 1 ELSE 0 END) OVER (...)&lt;/code&gt; to cumulative-sum a change flag. Once the island id is right, &lt;code&gt;GROUP BY user_id, island_id&lt;/code&gt; and aggregate — &lt;code&gt;MIN(ts)&lt;/code&gt; as start, &lt;code&gt;MAX(ts)&lt;/code&gt; as end, &lt;code&gt;COUNT(*)&lt;/code&gt; as length. Senior interviewers love this family because it tests window-function fluency without being about ranking, has both a portable answer (LAG + SUM) and a MATCH_RECOGNIZE alternative on Snowflake / Oracle, and generalises across sessionization, streaks, RLE, and SCD-2 with the same three-step template.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you sessionize an event stream in SQL?
&lt;/h3&gt;

&lt;p&gt;Sessionization uses the change-flag flavour of the gaps-and-islands template. In three lines: (1) compute the gap to the previous event with &lt;code&gt;LAG(event_time) OVER (PARTITION BY user_id ORDER BY event_time)&lt;/code&gt;; (2) flag &lt;code&gt;new_session = 1&lt;/code&gt; when the gap exceeds a threshold (usually 30 minutes per Google Analytics / Segment convention) or when there's no previous event; (3) cumulative-sum the flag with &lt;code&gt;SUM(new_session) OVER (PARTITION BY user_id ORDER BY event_time)&lt;/code&gt; to produce a per-user session id. Session-level aggregations then group by &lt;code&gt;(user_id, session_id)&lt;/code&gt; and pull &lt;code&gt;MIN(event_time)&lt;/code&gt; as session_start, &lt;code&gt;MAX(event_time)&lt;/code&gt; as session_end, &lt;code&gt;COUNT(*)&lt;/code&gt; as event_count, and per-event details via &lt;code&gt;MIN(...) FILTER (WHERE event_name = 'page_view')&lt;/code&gt; for the landing page. The primitive extends to explicit boundary events (sign-out closes a session) by adding a &lt;code&gt;LAG(event_name) = 'sign_out'&lt;/code&gt; branch to the CASE.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the row_number minus date trick?
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;row_number - date&lt;/code&gt; trick (also known as the Tabibitosan method) turns a dense-date sequence into constant island ids by subtracting the per-partition &lt;code&gt;ROW_NUMBER()&lt;/code&gt; from the ordered date attribute. When consecutive dates advance in lockstep with the row_number (2026-07-01 · rn=1, 2026-07-02 · rn=2, 2026-07-03 · rn=3), the subtraction yields the same constant (2026-06-30) inside the island — a natural GROUP BY key. When a gap appears (2026-07-06 · rn=4), the row_number moves ahead of the date and the subtraction produces a different constant (2026-07-02), correctly starting a new island. Every senior data engineer recognises this trick in ten seconds — practice writing it until you can produce the six-line CTE from memory. It's the fastest answer to LeetCode's "consecutive login days" problem and every senior data-engineering interview's dense-date island question.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you compute the longest login streak per user?
&lt;/h3&gt;

&lt;p&gt;Longest-streak-per-user is a four-CTE query on top of the Tabibitosan trick: (1) assign per-user &lt;code&gt;ROW_NUMBER()&lt;/code&gt; ordered by &lt;code&gt;login_date&lt;/code&gt;; (2) compute &lt;code&gt;island_id = login_date - rn * INTERVAL '1 day'&lt;/code&gt;; (3) group by &lt;code&gt;(user_id, island_id)&lt;/code&gt; to aggregate per-island &lt;code&gt;(streak_start, streak_end, streak_length)&lt;/code&gt;; (4) rank per user by &lt;code&gt;streak_length DESC, streak_end DESC&lt;/code&gt; and filter to &lt;code&gt;ROW_NUMBER() = 1&lt;/code&gt;. The output is one row per user with their longest ever streak. Current-streak-per-user swaps the ranking step for a filter &lt;code&gt;WHERE streak_end &amp;gt;= CURRENT_DATE - INTERVAL '1 day'&lt;/code&gt; — the &lt;code&gt;-1 day&lt;/code&gt; tolerance handles timezones. Gap-tolerant streaks (allow one-day misses) switch to the change-flag flavour with a 2-day boundary threshold. Business-day streaks pre-join a calendar table with a dense &lt;code&gt;business_day_num&lt;/code&gt; and use that as the ordered attribute. Every variation is a two-line change from the base recipe.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is SQL run length encoding and when do you reach for it?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;sql run length encoding&lt;/code&gt; compresses a sequence of state values into one row per contiguous run — &lt;code&gt;(state, run_start, run_end, run_length)&lt;/code&gt; — using the same LAG + change-flag primitive as sessionization but ordered by a general timestamp instead of a session boundary. The canonical query flags change points with &lt;code&gt;CASE WHEN state &amp;lt;&amp;gt; LAG(state) OVER (...) THEN 1 ELSE 0 END&lt;/code&gt;, cumulative-sums the flag into &lt;code&gt;run_id&lt;/code&gt;, then groups by &lt;code&gt;(state, run_id)&lt;/code&gt; to aggregate. Reach for RLE whenever a slowly-changing series has many consecutive same-value rows: feature-flag audit logs (10-100× compression), sensor readings, uptime pings, price plateaus, and SCD-2 slowly-changing dimensions (&lt;code&gt;valid_from&lt;/code&gt; / &lt;code&gt;valid_to&lt;/code&gt; history). Compression ratio equals the average run length; downstream analytical reads become dramatically cheaper. Snowflake and Oracle ship the declarative MATCH_RECOGNIZE alternative — &lt;code&gt;PATTERN (A B*) DEFINE B AS B.state = A.state&lt;/code&gt; — but the portable LAG + SUM version is the safer default for cross-warehouse code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which warehouses support MATCH_RECOGNIZE?
&lt;/h3&gt;

&lt;p&gt;MATCH_RECOGNIZE is the ANSI SQL/JSON row-pattern-matching clause that reads like a regex over ordered rows. Support in 2026: &lt;strong&gt;Oracle 12c+&lt;/strong&gt; (YES, the first ship in 2013), &lt;strong&gt;Snowflake&lt;/strong&gt; (YES, since 2021 GA), &lt;strong&gt;Postgres&lt;/strong&gt; (NO — no planned support as of 2026), &lt;strong&gt;BigQuery&lt;/strong&gt; (NO), &lt;strong&gt;SQL Server&lt;/strong&gt; (NO), &lt;strong&gt;MySQL 8&lt;/strong&gt; (NO), &lt;strong&gt;Redshift&lt;/strong&gt; (NO). For cross-warehouse projects, default to the portable LAG + &lt;code&gt;SUM(new_group) OVER&lt;/code&gt; pattern; it works everywhere and reads well once your team knows window functions cold. Reach for MATCH_RECOGNIZE only on Snowflake or Oracle when the pattern is genuinely multi-row and regex-like — sequences like "A then B+ then C" for fraud detection, click-stream funnel matching, or financial signal recognition. For simple RLE, sessionization, and streaks, the portable window-function recipe compiles to identical physical plans and doesn't fork your codebase across dialects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practice on PipeCode
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Drill the &lt;a href="https://pipecode.ai/explore/practice/topic/gaps-and-islands" rel="noopener noreferrer"&gt;gaps-and-islands practice library →&lt;/a&gt; for date-island detection, sessionization, streak counting, and RLE reps across every warehouse dialect.&lt;/li&gt;
&lt;li&gt;Rehearse on &lt;a href="https://pipecode.ai/explore/practice/topic/window-functions" rel="noopener noreferrer"&gt;window-function problems →&lt;/a&gt; — the ROW_NUMBER, LAG, LEAD, and cumulative SUM primitives that every gaps-and-islands answer builds on.&lt;/li&gt;
&lt;li&gt;Sharpen &lt;a href="https://pipecode.ai/explore/practice/topic/window-functions/sql" rel="noopener noreferrer"&gt;SQL window function drills →&lt;/a&gt; for the dialect-portable half — Postgres, Snowflake, BigQuery, SQL Server, MySQL.&lt;/li&gt;
&lt;li&gt;Push the difficulty ceiling with &lt;a href="https://pipecode.ai/explore/practice/topic/window-functions/sql/medium" rel="noopener noreferrer"&gt;medium window-function problems →&lt;/a&gt; and the &lt;a href="https://pipecode.ai/explore/practice/topic/window-functions/sql/hard" rel="noopener noreferrer"&gt;hard tier →&lt;/a&gt; for streak, sessionization, and RLE senior interview scenarios.&lt;/li&gt;
&lt;li&gt;Practise &lt;a href="https://pipecode.ai/explore/practice/topic/sliding-window" rel="noopener noreferrer"&gt;sliding-window problems →&lt;/a&gt; for the rolling-aggregate cousin of gaps-and-islands.&lt;/li&gt;
&lt;li&gt;Layer &lt;a href="https://pipecode.ai/explore/practice/topic/time-series" rel="noopener noreferrer"&gt;time-series drills →&lt;/a&gt; for the timestamp-heavy contexts where sessionization and streaks live.&lt;/li&gt;
&lt;li&gt;Stack the &lt;a href="https://pipecode.ai/explore/practice/topic/interval-processing" rel="noopener noreferrer"&gt;interval-processing library →&lt;/a&gt; for uptime windows, overlap detection, and RLE variations.&lt;/li&gt;
&lt;li&gt;For general SQL sharpening, work through the &lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;SQL problem library →&lt;/a&gt; which contains 450+ DE-focused questions.&lt;/li&gt;
&lt;li&gt;For the broader SQL interview surface, take the &lt;a href="https://pipecode.ai/explore/courses/sql-for-data-engineering-interviews-from-zero-to-faang" rel="noopener noreferrer"&gt;SQL for Data Engineering course →&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/" rel="noopener noreferrer"&gt;Pipecode.ai&lt;/a&gt; is Leetcode for Data Engineering — every `sql gaps and islands` recipe above ships with hands-on practice rooms where you write the Tabibitosan island-id derivation, wire the 30-minute sessionization pattern, chase the longest-streak-per-user query, compress a state series with RLE, and rehearse the MATCH_RECOGNIZE alternative against real graded inputs. PipeCode pairs every reading with 450+ DE-focused problems and a real-time scoring engine, so you never have to wonder whether your `consecutive rows sql` answer holds up under a senior interviewer's depth probes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/gaps-and-islands" rel="noopener noreferrer"&gt;Practice gaps and islands now →&lt;/a&gt;&lt;br&gt;
&lt;a href="https://pipecode.ai/explore/practice/topic/window-functions" rel="noopener noreferrer"&gt;Window function drills →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>sql</category>
      <category>interview</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>SQL JSON Functions: JSON_VALUE, JSON_EXTRACT, JSONB, JSON_TABLE Across Dialects</title>
      <dc:creator>Gowtham Potureddi</dc:creator>
      <pubDate>Fri, 10 Jul 2026 17:32:28 +0000</pubDate>
      <link>https://dev.to/gowthampotureddi/sql-json-functions-jsonvalue-jsonextract-jsonb-jsontable-across-dialects-3efn</link>
      <guid>https://dev.to/gowthampotureddi/sql-json-functions-jsonvalue-jsonextract-jsonb-jsontable-across-dialects-3efn</guid>
      <description>&lt;p&gt;&lt;strong&gt;&lt;code&gt;sql json&lt;/code&gt;&lt;/strong&gt; is the single most-searched semi-structured keyword in modern warehouse SQL — and the single most dialect-fractured one in 2026. The same "extract &lt;code&gt;user.id&lt;/code&gt; out of an event payload and use it as a join key" ask has five different answers across the five engines most data teams live in: Postgres uses &lt;code&gt;payload-&amp;gt;'user'-&amp;gt;&amp;gt;'id'&lt;/code&gt; or &lt;code&gt;jsonb_path_query&lt;/code&gt;, MySQL 8 ships &lt;code&gt;JSON_EXTRACT(payload, '$.user.id')&lt;/code&gt; and the &lt;code&gt;-&amp;gt;&amp;gt;&lt;/code&gt; shortcut, SQL Server ships &lt;code&gt;JSON_VALUE(payload, '$.user.id')&lt;/code&gt;, Snowflake reaches for its dot-notation &lt;code&gt;payload:user.id::string&lt;/code&gt; on top of a &lt;code&gt;VARIANT&lt;/code&gt; column, and BigQuery ships &lt;code&gt;JSON_VALUE(payload, '$.user.id')&lt;/code&gt; against a native &lt;code&gt;JSON&lt;/code&gt; type. There is no portable spelling of a JSON path; the interviewer wants to hear you say that out loud in the first sentence.&lt;/p&gt;

&lt;p&gt;This guide is the mid-to-senior comparison you wished existed the first time an interviewer asked you to write a &lt;code&gt;json_value sql&lt;/code&gt; expression on the whiteboard, sketch a &lt;code&gt;json_extract sql&lt;/code&gt; join in MySQL, wire a &lt;code&gt;jsonb postgres&lt;/code&gt; GIN index for a hot containment filter, flatten a nested payload with &lt;code&gt;json_table sql&lt;/code&gt;, chase a schema-drift regression in &lt;code&gt;bigquery json&lt;/code&gt;, or design a &lt;code&gt;snowflake variant&lt;/code&gt; column for a fast-changing event stream. It walks through the JSONPath grammar every engine borrows (&lt;code&gt;$&lt;/code&gt; root, &lt;code&gt;.key&lt;/code&gt; child, &lt;code&gt;[i]&lt;/code&gt; index, &lt;code&gt;[*]&lt;/code&gt; wildcard, &lt;code&gt;..desc&lt;/code&gt; recursive descent), the scalar-vs-sub-tree split that separates &lt;code&gt;JSON_VALUE&lt;/code&gt; from &lt;code&gt;JSON_QUERY&lt;/code&gt; and Postgres &lt;code&gt;-&amp;gt;&amp;gt;&lt;/code&gt; from &lt;code&gt;-&amp;gt;&lt;/code&gt;, the Postgres &lt;code&gt;jsonb&lt;/code&gt; type with its &lt;code&gt;jsonb_ops&lt;/code&gt; and &lt;code&gt;jsonb_path_ops&lt;/code&gt; GIN indexes plus the &lt;code&gt;@&amp;gt;&lt;/code&gt; containment operator, the ANSI &lt;code&gt;JSON_TABLE&lt;/code&gt; clause with its &lt;code&gt;NESTED PATH&lt;/code&gt; sub-block and its Snowflake &lt;code&gt;LATERAL FLATTEN&lt;/code&gt;, BigQuery &lt;code&gt;UNNEST(JSON_QUERY_ARRAY(...))&lt;/code&gt;, and Postgres &lt;code&gt;jsonb_to_recordset&lt;/code&gt; equivalents, and the storage-plus-index performance matrix across all five engines including when a materialised generated column beats every path expression. Each section pairs a teaching block with a Solution-Tail interview answer — code, a step-by-step trace, an output table, then a concept-by-concept breakdown of why it works.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2FPipecode-pull-zone.b-cdn.net%2Fblog-images%2Fsql-json-functions-across-dialects%2Fsql-json-functions-across-dialects-header.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2FPipecode-pull-zone.b-cdn.net%2Fblog-images%2Fsql-json-functions-across-dialects%2Fsql-json-functions-across-dialects-header.jpeg" alt="PipeCode blog header for SQL JSON functions across dialects — bold white headline 'SQL JSON Functions' with subtitle 'JSON_VALUE · JSON_EXTRACT · JSONB · JSON_TABLE' and a stylised JSON path extraction card on a dark gradient with purple, green, orange, and blue accents and a small pipecode.ai attribution." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you want &lt;strong&gt;hands-on reps&lt;/strong&gt; immediately after reading, drill the &lt;a href="https://pipecode.ai/explore/practice/topic/json" rel="noopener noreferrer"&gt;JSON practice library →&lt;/a&gt;, rehearse on &lt;a href="https://pipecode.ai/explore/practice/topic/parsing" rel="noopener noreferrer"&gt;parsing problems →&lt;/a&gt;, and sharpen the reshape axis with &lt;a href="https://pipecode.ai/explore/practice/topic/data-transformation" rel="noopener noreferrer"&gt;data-transformation drills →&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;On this page&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why JSON in SQL matters in 2026&lt;/li&gt;
&lt;li&gt;JSON_VALUE / JSON_EXTRACT — path expressions&lt;/li&gt;
&lt;li&gt;JSONB in Postgres — index it or regret it&lt;/li&gt;
&lt;li&gt;JSON_TABLE — flatten JSON to rows&lt;/li&gt;
&lt;li&gt;Dialect matrix + performance&lt;/li&gt;
&lt;li&gt;Cheat sheet — SQL JSON recipe list&lt;/li&gt;
&lt;li&gt;Frequently asked questions&lt;/li&gt;
&lt;li&gt;Practice on PipeCode&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1. Why JSON in SQL matters in 2026
&lt;/h2&gt;

&lt;h3&gt;
  
  
  JSON columns are the default event-payload shape — and the mental model every senior data engineer needs to lock in on day one
&lt;/h3&gt;

&lt;p&gt;The one-sentence invariant: &lt;strong&gt;a &lt;code&gt;sql json&lt;/code&gt; column stores semi-structured data whose schema drifts faster than the surrounding relational model, and every senior interviewer expects you to name the dialect's storage type, its path syntax, its index options, and the flattening operator in the same breath&lt;/strong&gt;. Once you internalise that "storage type + path syntax + index shape + flatten operator" is the whole design space, the JSON-in-SQL interview surface collapses into a four-cell mental card per dialect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Four axes interviewers actually probe.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Storage type.&lt;/strong&gt; Postgres has both &lt;code&gt;json&lt;/code&gt; (raw text, ordered, no dedup) and &lt;code&gt;jsonb&lt;/code&gt; (parsed, deduped, binary, indexable); MySQL 8 has one binary &lt;code&gt;JSON&lt;/code&gt; type; SQL Server piggybacks on &lt;code&gt;NVARCHAR(MAX)&lt;/code&gt; with &lt;code&gt;ISJSON&lt;/code&gt; validation; Snowflake has &lt;code&gt;VARIANT&lt;/code&gt; (columnar, self-describing); BigQuery has a native &lt;code&gt;JSON&lt;/code&gt; type on top of the older &lt;code&gt;STRING&lt;/code&gt;-of-JSON pattern. Every choice trades parse cost, storage size, and query cost against schema flexibility.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Path syntax.&lt;/strong&gt; The five engines share the JSONPath grammar (&lt;code&gt;$&lt;/code&gt; root, &lt;code&gt;.key&lt;/code&gt; child, &lt;code&gt;[i]&lt;/code&gt; index, &lt;code&gt;[*]&lt;/code&gt; wildcard) but disagree on the wrapper: Postgres exposes &lt;code&gt;-&amp;gt;&lt;/code&gt;, &lt;code&gt;-&amp;gt;&amp;gt;&lt;/code&gt;, &lt;code&gt;#&amp;gt;&lt;/code&gt;, &lt;code&gt;#&amp;gt;&amp;gt;&lt;/code&gt; arrow operators plus SQL/JSON &lt;code&gt;jsonb_path_query&lt;/code&gt;; MySQL 8 ships &lt;code&gt;JSON_EXTRACT&lt;/code&gt; and the &lt;code&gt;-&amp;gt;&amp;gt;&lt;/code&gt; shortcut; SQL Server ships &lt;code&gt;JSON_VALUE&lt;/code&gt; and &lt;code&gt;JSON_QUERY&lt;/code&gt;; Snowflake extends the SQL grammar with &lt;code&gt;col:key.sub.notation&lt;/code&gt;; BigQuery ships &lt;code&gt;JSON_VALUE&lt;/code&gt;, &lt;code&gt;JSON_QUERY&lt;/code&gt;, and a dot-syntax on the native &lt;code&gt;JSON&lt;/code&gt; type.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalar vs sub-tree.&lt;/strong&gt; Every dialect distinguishes "extract a scalar and coerce it to a SQL type" from "extract a JSON sub-tree that stays JSON." SQL Server draws the line at &lt;code&gt;JSON_VALUE&lt;/code&gt; (scalar) vs &lt;code&gt;JSON_QUERY&lt;/code&gt; (sub-tree); Postgres draws it at &lt;code&gt;-&amp;gt;&amp;gt;&lt;/code&gt; (scalar text) vs &lt;code&gt;-&amp;gt;&lt;/code&gt; (JSON child); MySQL draws it at &lt;code&gt;-&amp;gt;&amp;gt;&lt;/code&gt; vs &lt;code&gt;-&amp;gt;&lt;/code&gt;; BigQuery draws it at &lt;code&gt;JSON_VALUE&lt;/code&gt; vs &lt;code&gt;JSON_QUERY&lt;/code&gt;. Missing this split is the most-asked interview correction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flatten to rows.&lt;/strong&gt; Nested arrays cannot be joined against a relational query without a flattening operator. The ANSI answer is &lt;code&gt;JSON_TABLE&lt;/code&gt; (Oracle, MySQL 8, SQL Server 2022); Snowflake ships &lt;code&gt;LATERAL FLATTEN&lt;/code&gt;; BigQuery pairs &lt;code&gt;UNNEST&lt;/code&gt; with &lt;code&gt;JSON_QUERY_ARRAY&lt;/code&gt;; Postgres uses &lt;code&gt;jsonb_array_elements&lt;/code&gt;, &lt;code&gt;jsonb_to_recordset&lt;/code&gt;, or a &lt;code&gt;LATERAL&lt;/code&gt; join.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Where semi-structured data lands in your pipelines.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Event tracking payloads.&lt;/strong&gt; Segment, Rudderstack, Snowplow, and PostHog all emit &lt;code&gt;event(id, timestamp, event_type, payload jsonb)&lt;/code&gt; schemas. The event-type-specific columns live inside &lt;code&gt;payload&lt;/code&gt; because they change too fast to materialise as columns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Third-party API bodies.&lt;/strong&gt; Webhooks from Stripe, Shopify, Zendesk, HubSpot, and 100 other SaaS integrations arrive as JSON. Landing them in a JSON column preserves the source-of-truth shape; downstream transforms extract typed columns as they stabilise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CDC envelopes.&lt;/strong&gt; Debezium, Fivetran, Airbyte, and Meltano wrap each row change in a JSON envelope with &lt;code&gt;before&lt;/code&gt;, &lt;code&gt;after&lt;/code&gt;, &lt;code&gt;op&lt;/code&gt;, &lt;code&gt;source&lt;/code&gt;, &lt;code&gt;ts_ms&lt;/code&gt; keys. Consumer pipelines routinely &lt;code&gt;JSON_EXTRACT(payload, '$.after.user_id')&lt;/code&gt; to project the changed row.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Config and feature flags.&lt;/strong&gt; LaunchDarkly, GrowthBook, and internal feature-flag tables often store variant assignments as a JSON blob per user. Filtering on &lt;code&gt;payload-&amp;gt;&amp;gt;'variant'&lt;/code&gt; is a hot analytics path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semi-structured logs.&lt;/strong&gt; Application logs (structured JSON) land as JSON columns for indexing; &lt;code&gt;SELECT * FROM logs WHERE payload @&amp;gt; '{"level":"error"}'&lt;/code&gt; is a common Postgres pattern.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When JSON-in-SQL beats a separate document store.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One system, one backup, one auth model.&lt;/strong&gt; Running MongoDB or DynamoDB alongside your warehouse doubles your operational surface. Modern warehouses ship JSON support strong enough that most read-heavy workloads never leave SQL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Joins with the relational world.&lt;/strong&gt; Every event has a &lt;code&gt;user_id&lt;/code&gt; that joins against your &lt;code&gt;users&lt;/code&gt; table. Doing that join across a warehouse and MongoDB is painful; doing it inside Postgres with &lt;code&gt;jsonb&lt;/code&gt; extraction is a two-line query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analytics-friendly shape.&lt;/strong&gt; Once you flatten with &lt;code&gt;JSON_TABLE&lt;/code&gt; or &lt;code&gt;LATERAL FLATTEN&lt;/code&gt;, you get a relational shape that every BI tool understands. Grafana, Metabase, Superset, Looker — every dashboard tool wants columns, not documents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost.&lt;/strong&gt; Snowflake &lt;code&gt;VARIANT&lt;/code&gt; and BigQuery native &lt;code&gt;JSON&lt;/code&gt; push semi-structured storage into columnar formats. Query cost on a 10TB event log is often lower than a document store's query cost on the same shape.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When a separate document store still wins.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sub-10ms point-read latency.&lt;/strong&gt; MongoDB and DynamoDB are optimised for OLTP-style single-document reads. Warehouses are not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deep nesting with heavy write-heavy update patterns.&lt;/strong&gt; JSONB updates in Postgres rewrite the whole binary blob; MongoDB updates individual sub-documents. If your workload is 10× writes per read on a big nested doc, a document store fits better.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write throughput above 100k QPS.&lt;/strong&gt; Warehouses are batch-write friendly; document stores handle single-row streaming writes better.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What senior interviewers probe on JSON-in-SQL.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Path vs pointer.&lt;/strong&gt; JSONPath (&lt;code&gt;$.a.b.c&lt;/code&gt;) is the query grammar; JSON Pointer (&lt;code&gt;/a/b/c&lt;/code&gt;) is the addressing grammar (RFC 6901). Both exist; pointers are what Postgres &lt;code&gt;#&amp;gt;&lt;/code&gt; and &lt;code&gt;#&amp;gt;&amp;gt;&lt;/code&gt; use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalar vs sub-tree.&lt;/strong&gt; Do you say "&lt;code&gt;JSON_VALUE&lt;/code&gt; is scalar, &lt;code&gt;JSON_QUERY&lt;/code&gt; is sub-tree" without prompting? Do you know Postgres draws the same line at &lt;code&gt;-&amp;gt;&amp;gt;&lt;/code&gt; vs &lt;code&gt;-&amp;gt;&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage cost.&lt;/strong&gt; &lt;code&gt;jsonb&lt;/code&gt; is ~10% smaller than &lt;code&gt;json&lt;/code&gt; (binary encoded, deduped keys), Snowflake &lt;code&gt;VARIANT&lt;/code&gt; is columnar-encoded, BigQuery native &lt;code&gt;JSON&lt;/code&gt; beats &lt;code&gt;STRING&lt;/code&gt;-of-JSON for both storage and query cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Index shape.&lt;/strong&gt; GIN indexes with &lt;code&gt;jsonb_ops&lt;/code&gt; (universal, larger) vs &lt;code&gt;jsonb_path_ops&lt;/code&gt; (containment-only, smaller). When to add a functional expression index on a single path. When to materialise a generated column instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When to flatten.&lt;/strong&gt; Every join key, filter key, and group-by key that comes out of JSON should be materialised as a column. Every read-only projection can stay in JSON. Naming this rule out loud is the senior signal.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Worked example — the same "extract user id" ask in five dialects
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The canonical warm-up interview ask — given an events table with a JSON payload, extract &lt;code&gt;user_id&lt;/code&gt; and count events per user — reads identically as an English requirement across every warehouse. The SQL to express it looks wildly different by dialect. Writing the same extraction five ways builds a mental dialect matrix that pays off in every JSON follow-up question.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given an &lt;code&gt;events&lt;/code&gt; table with &lt;code&gt;(event_id, ts, payload)&lt;/code&gt; where &lt;code&gt;payload&lt;/code&gt; is a JSON column containing at minimum a &lt;code&gt;user_id&lt;/code&gt; key, write a query that counts events per user in Postgres (jsonb), MySQL 8, SQL Server, Snowflake, and BigQuery.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;event_id&lt;/th&gt;
&lt;th&gt;ts&lt;/th&gt;
&lt;th&gt;payload&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2026-07-01&lt;/td&gt;
&lt;td&gt;{"user_id": "u1", "kind": "click"}&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2026-07-01&lt;/td&gt;
&lt;td&gt;{"user_id": "u1", "kind": "view"}&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2026-07-01&lt;/td&gt;
&lt;td&gt;{"user_id": "u2", "kind": "click"}&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;2026-07-02&lt;/td&gt;
&lt;td&gt;{"user_id": "u1", "kind": "purchase"}&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Postgres (jsonb) — arrow operator shortcut&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'user_id'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'user_id'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- MySQL 8 — JSON_EXTRACT + JSON_UNQUOTE (or the -&amp;gt;&amp;gt; shortcut)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'$.user_id'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'$.user_id'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- SQL Server — JSON_VALUE&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.user_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.user_id'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Snowflake — dot-notation VARIANT access + cast&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- BigQuery — JSON_VALUE on native JSON type&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.user_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.user_id'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Postgres's &lt;code&gt;payload-&amp;gt;&amp;gt;'user_id'&lt;/code&gt; is the "arrow followed by arrow followed by greater-than" operator that reads: "extract the &lt;code&gt;user_id&lt;/code&gt; key and return the value as text." The double-arrow &lt;code&gt;-&amp;gt;&amp;gt;&lt;/code&gt; unwraps the JSON string to a SQL &lt;code&gt;text&lt;/code&gt; — critical for &lt;code&gt;GROUP BY&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;MySQL 8's &lt;code&gt;payload-&amp;gt;&amp;gt;'$.user_id'&lt;/code&gt; is a syntactic shortcut for &lt;code&gt;JSON_UNQUOTE(JSON_EXTRACT(payload, '$.user_id'))&lt;/code&gt;. Same mechanics as Postgres's &lt;code&gt;-&amp;gt;&amp;gt;&lt;/code&gt; but the path is JSONPath (with &lt;code&gt;$.&lt;/code&gt; prefix) rather than a bare key name.&lt;/li&gt;
&lt;li&gt;SQL Server's &lt;code&gt;JSON_VALUE(payload, '$.user_id')&lt;/code&gt; returns a scalar SQL string. It errors if the target is a JSON object or array — &lt;code&gt;JSON_VALUE&lt;/code&gt; is scalar-only. For sub-tree extraction you would reach for &lt;code&gt;JSON_QUERY&lt;/code&gt; instead.&lt;/li&gt;
&lt;li&gt;Snowflake's &lt;code&gt;payload:user_id::string&lt;/code&gt; uses colon-notation for the key access and the SQL &lt;code&gt;::&lt;/code&gt; cast operator to force the return type. Without the cast, the value stays a &lt;code&gt;VARIANT&lt;/code&gt; — which is fine for further chaining but often surprises consumers who expect a &lt;code&gt;VARCHAR&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;BigQuery's &lt;code&gt;JSON_VALUE(payload, '$.user_id')&lt;/code&gt; looks like SQL Server's but runs against the native &lt;code&gt;JSON&lt;/code&gt; type. If your column is &lt;code&gt;STRING&lt;/code&gt;-of-JSON, use &lt;code&gt;JSON_EXTRACT_SCALAR&lt;/code&gt; instead — a common &lt;code&gt;bigquery json&lt;/code&gt; gotcha.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;n&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; In every dialect, always cast or unwrap the extracted value to a SQL scalar type before &lt;code&gt;GROUP BY&lt;/code&gt;, &lt;code&gt;JOIN&lt;/code&gt;, or &lt;code&gt;WHERE&lt;/code&gt;. Grouping on a raw JSON sub-tree produces different equality semantics per dialect and is almost always a bug.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — sub-tree vs scalar (JSON_VALUE vs JSON_QUERY)
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The most common junior-to-senior correction: candidates reach for &lt;code&gt;JSON_VALUE&lt;/code&gt; to grab a nested object and hit "the requested JSON path expression is not a scalar." The interviewer wants to see you know that &lt;code&gt;JSON_VALUE&lt;/code&gt; returns a scalar, &lt;code&gt;JSON_QUERY&lt;/code&gt; returns a sub-tree, and the same split exists in Postgres (&lt;code&gt;-&amp;gt;&amp;gt;&lt;/code&gt; vs &lt;code&gt;-&amp;gt;&lt;/code&gt;), MySQL (&lt;code&gt;-&amp;gt;&amp;gt;&lt;/code&gt; vs &lt;code&gt;-&amp;gt;&lt;/code&gt;), and Snowflake (&lt;code&gt;::string&lt;/code&gt; cast vs raw &lt;code&gt;VARIANT&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given a payload &lt;code&gt;{"user": {"id": "u1", "name": "Ada"}, "kind": "click"}&lt;/code&gt;, write two queries per dialect (SQL Server + Postgres): one that returns the &lt;code&gt;user&lt;/code&gt; sub-tree as JSON, and one that returns the &lt;code&gt;user.id&lt;/code&gt; scalar as a SQL string. Explain the error you'd hit if you mis-used them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;payload&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;{"user": {"id": "u1", "name": "Ada"}, "kind": "click"}&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;{"user": {"id": "u2", "name": "Bo"}, "kind": "view"}&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- SQL Server — JSON_QUERY for sub-tree, JSON_VALUE for scalar&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;JSON_QUERY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.user'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;user_json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- returns JSON object&lt;/span&gt;
  &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.user.id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;      &lt;span class="c1"&gt;-- returns scalar&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- WRONG — JSON_VALUE on a sub-tree returns NULL (or errors depending on option)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.user'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Postgres (jsonb) — -&amp;gt; for sub-tree, -&amp;gt;&amp;gt; for scalar&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'user'&lt;/span&gt;              &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;user_json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- returns jsonb&lt;/span&gt;
  &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'user'&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'id'&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;      &lt;span class="c1"&gt;-- returns text&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Alt — Postgres path pointer #&amp;gt; (sub-tree), #&amp;gt;&amp;gt; (scalar text)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;#&amp;gt;&lt;/span&gt;  &lt;span class="s1"&gt;'{user}'&lt;/span&gt;           &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;user_json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'{user,id}'&lt;/span&gt;        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;JSON_QUERY&lt;/code&gt; and &lt;code&gt;JSON_VALUE&lt;/code&gt; are complementary: one returns a JSON sub-tree (still JSON), the other returns a scalar coerced to a SQL type. Mixing them up is the most-asked correction.&lt;/li&gt;
&lt;li&gt;In SQL Server, &lt;code&gt;JSON_VALUE(payload, '$.user')&lt;/code&gt; on an object target returns &lt;code&gt;NULL&lt;/code&gt; in lax mode (default) or errors in strict mode. Neither is what you want — reach for &lt;code&gt;JSON_QUERY&lt;/code&gt; instead.&lt;/li&gt;
&lt;li&gt;Postgres's &lt;code&gt;-&amp;gt;&lt;/code&gt; returns a &lt;code&gt;jsonb&lt;/code&gt; child; &lt;code&gt;-&amp;gt;&amp;gt;&lt;/code&gt; returns the text representation of a scalar child. The double-arrow always unwraps quotes; the single-arrow leaves them.&lt;/li&gt;
&lt;li&gt;Postgres's &lt;code&gt;#&amp;gt;&lt;/code&gt; and &lt;code&gt;#&amp;gt;&amp;gt;&lt;/code&gt; are the path-pointer variants that accept a text-array of keys. &lt;code&gt;payload #&amp;gt;&amp;gt; '{user,id}'&lt;/code&gt; is equivalent to &lt;code&gt;payload -&amp;gt; 'user' -&amp;gt;&amp;gt; 'id'&lt;/code&gt;. Path pointers are useful for programmatic construction; arrow chains are more readable for humans.&lt;/li&gt;
&lt;li&gt;When the extracted value is used as a &lt;code&gt;GROUP BY&lt;/code&gt; key or a &lt;code&gt;JOIN&lt;/code&gt; target, always use the scalar form — &lt;code&gt;JSON_VALUE&lt;/code&gt; in SQL Server, &lt;code&gt;-&amp;gt;&amp;gt;&lt;/code&gt; in Postgres/MySQL, &lt;code&gt;::string&lt;/code&gt; in Snowflake, &lt;code&gt;JSON_VALUE&lt;/code&gt; in BigQuery. The sub-tree form is only for further JSON chaining or for embedding in another JSON output.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;user_json&lt;/th&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;{"id": "u1", "name": "Ada"}&lt;/td&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;{"id": "u2", "name": "Bo"}&lt;/td&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Every extraction is either scalar (headed for a SQL type) or sub-tree (headed for further JSON work). Name the direction before you write the operator — the operator falls out of the direction.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the JSONPath grammar every engine borrows
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; JSONPath is the ANSI-flavoured grammar every engine implements. The five must-know constructs are &lt;code&gt;$&lt;/code&gt; (root), &lt;code&gt;.key&lt;/code&gt; (child by name), &lt;code&gt;[i]&lt;/code&gt; (child by index), &lt;code&gt;[*]&lt;/code&gt; (wildcard over an array), and &lt;code&gt;..desc&lt;/code&gt; (recursive descent). Once you know the grammar, the dialect surface reduces to "which function wraps the path?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given a payload with a nested &lt;code&gt;orders&lt;/code&gt; array &lt;code&gt;{"user_id": "u1", "orders": [{"id": 1, "total": 10}, {"id": 2, "total": 25}]}&lt;/code&gt;, write the JSONPath expressions to extract (a) the &lt;code&gt;user_id&lt;/code&gt;, (b) the first order's &lt;code&gt;total&lt;/code&gt;, (c) all order totals, (d) any &lt;code&gt;id&lt;/code&gt; anywhere in the payload. Show one dialect for each — Postgres &lt;code&gt;jsonb_path_query&lt;/code&gt;, MySQL &lt;code&gt;JSON_EXTRACT&lt;/code&gt;, SQL Server &lt;code&gt;JSON_VALUE / JSON_QUERY&lt;/code&gt;, BigQuery &lt;code&gt;JSON_VALUE / JSON_QUERY_ARRAY&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;payload&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;{"user_id": "u1", "orders": [{"id": 1, "total": 10}, {"id": 2, "total": 25}]}&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Postgres — SQL/JSON jsonb_path_query&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;jsonb_path_query_first&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.user_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;jsonb_path_query_first&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.orders[0].total'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;first_total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;jsonb_path_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.orders[*].total'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;every_total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;jsonb_path_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$..id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;every_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- MySQL 8 — JSON_EXTRACT&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;JSON_EXTRACT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.user_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;JSON_EXTRACT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.orders[0].total'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;first_total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;JSON_EXTRACT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.orders[*].total'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;every_total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;JSON_EXTRACT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$..id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;every_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- SQL Server — JSON_VALUE (scalar), JSON_QUERY (sub-tree / array)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.user_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;              &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.orders[0].total'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;first_total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;JSON_QUERY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.orders[*].total'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;every_total&lt;/span&gt;  &lt;span class="c1"&gt;-- array&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Note: SQL Server has no ..desc recursive-descent operator.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- BigQuery — JSON_VALUE, JSON_QUERY_ARRAY&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.user_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.orders[0].total'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;first_total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;JSON_QUERY_ARRAY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.orders'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;every_order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;JSON_QUERY_ARRAY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$..id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                      &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;every_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;$&lt;/code&gt; is the root — every JSONPath starts with it. &lt;code&gt;$.user_id&lt;/code&gt; reads "from the root, take the &lt;code&gt;user_id&lt;/code&gt; child."&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;[i]&lt;/code&gt; is index access. &lt;code&gt;$.orders[0]&lt;/code&gt; picks the first element; &lt;code&gt;$.orders[-1]&lt;/code&gt; picks the last (dialect-dependent — Postgres and BigQuery support negative indexing, SQL Server does not).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;[*]&lt;/code&gt; is the wildcard — "every element of this array." Combined with a child key, &lt;code&gt;$.orders[*].total&lt;/code&gt; returns every order's total. The return shape is an array when the wrapper function returns arrays (&lt;code&gt;JSON_QUERY&lt;/code&gt;), or a set of rows when the wrapper is set-returning (&lt;code&gt;jsonb_path_query&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;..key&lt;/code&gt; is recursive descent — "find &lt;code&gt;key&lt;/code&gt; anywhere in the sub-tree." Postgres, MySQL, and BigQuery support it; SQL Server does not. Great for pulling a value out of a deeply nested payload without knowing the exact path.&lt;/li&gt;
&lt;li&gt;Every dialect maps the same JSONPath grammar to a different wrapper: Postgres &lt;code&gt;jsonb_path_query&lt;/code&gt; (set-returning) or &lt;code&gt;jsonb_path_query_first&lt;/code&gt; (scalar), MySQL &lt;code&gt;JSON_EXTRACT&lt;/code&gt; (returns a JSON value that may be an array), SQL Server &lt;code&gt;JSON_VALUE&lt;/code&gt; (scalar) or &lt;code&gt;JSON_QUERY&lt;/code&gt; (sub-tree), BigQuery &lt;code&gt;JSON_VALUE&lt;/code&gt; (scalar) or &lt;code&gt;JSON_QUERY&lt;/code&gt; / &lt;code&gt;JSON_QUERY_ARRAY&lt;/code&gt; (sub-tree/array).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output (Postgres set-returning).&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;first_total&lt;/th&gt;
&lt;th&gt;every_total&lt;/th&gt;
&lt;th&gt;every_id&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"u1"&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"u1"&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Memorise the five JSONPath constructs (&lt;code&gt;$&lt;/code&gt;, &lt;code&gt;.key&lt;/code&gt;, &lt;code&gt;[i]&lt;/code&gt;, &lt;code&gt;[*]&lt;/code&gt;, &lt;code&gt;..desc&lt;/code&gt;) once. After that, the dialect surface is just "which function wraps the path?" and "does the function return a scalar, a JSON, or a set of rows?"&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on the JSON-in-SQL mental model
&lt;/h3&gt;

&lt;p&gt;A senior interviewer often opens with: "You have a table &lt;code&gt;events(id, ts, payload)&lt;/code&gt; where &lt;code&gt;payload&lt;/code&gt; is a JSON column. Walk me through your decision tree for whether to keep a value inside the JSON, materialise it as a generated column, or copy it into a separate typed column. What's your rule of thumb?"&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using the "hot path materialisation" decision framework
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Decision framework — keep in JSON, materialise, or extract to a column

1. Is the key on the hot filter path (WHERE payload-&amp;gt;&amp;gt;'key' = ...)?
   yes → materialise as a generated column + index it
   no  → keep in JSON

2. Is the key a JOIN target?
   yes → materialise as a generated column + index it
   no  → keep in JSON

3. Is the key a GROUP BY or ORDER BY key?
   yes → materialise as a generated column
   no  → keep in JSON

4. Does the key have a stable presence across all rows?
   yes → materialise (or promote to a plain column)
   no  → keep in JSON

5. Is the key part of a schema-drift-heavy sub-tree
   (rarely-queried event-type-specific fields)?
   yes → keep in JSON forever
   no  → consider promotion to a plain column
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Q1 hot filter&lt;/th&gt;
&lt;th&gt;Q2 join&lt;/th&gt;
&lt;th&gt;Q3 group/order&lt;/th&gt;
&lt;th&gt;Q4 stable&lt;/th&gt;
&lt;th&gt;Q5 drift&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;user_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;Promote to a plain column&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;event_type&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;Materialise + index&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;session_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;sometimes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;Materialise (no index)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;orders[*].total&lt;/code&gt; (array agg)&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;Keep in JSON, flatten at query time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;custom_flags&lt;/code&gt; (freeform tags)&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;Keep in JSON forever&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The framework turns "should I extract this key?" from a taste question into a mechanical decision. Every key on the hot query path pays for materialisation with much better plan quality; every drifty key stays in the JSON payload where it belongs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;When it wins&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Promote to plain column&lt;/td&gt;
&lt;td&gt;Stable + always-present + hot filter/join/group key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Materialise as generated column + index&lt;/td&gt;
&lt;td&gt;Stable + hot filter path + still expressible as a JSON extract&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Materialise as generated column (no index)&lt;/td&gt;
&lt;td&gt;Hot projection but not filter/join key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keep in JSON, flatten at query time&lt;/td&gt;
&lt;td&gt;Nested arrays consumed by rare reports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keep in JSON forever&lt;/td&gt;
&lt;td&gt;Schema-drift heavy, low-frequency reads&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Hot path materialisation&lt;/strong&gt;&lt;/strong&gt; — every warehouse has a query planner that struggles with expression-based filters. &lt;code&gt;WHERE payload-&amp;gt;&amp;gt;'user_id' = 'u1'&lt;/code&gt; triggers a full scan unless the expression is indexed. Materialising the extraction into a real column exposes it to the planner and unlocks index-based filtering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Generated columns preserve the source of truth&lt;/strong&gt;&lt;/strong&gt; — a &lt;code&gt;GENERATED ALWAYS AS (payload-&amp;gt;&amp;gt;'user_id') STORED&lt;/code&gt; column stays in sync with the JSON automatically. You get the query-time win of a materialised column and the schema-drift resilience of the JSON payload in the same table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Schema drift is a first-class concern&lt;/strong&gt;&lt;/strong&gt; — the reason data engineers reach for JSON at all is that the schema changes weekly. Freezing every field into a plain column defeats the purpose. The framework keeps the drifty fields in JSON and only promotes the stable, high-value ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Stable + hot signals both matter&lt;/strong&gt;&lt;/strong&gt; — a field that's stable but never queried doesn't need materialisation; a hot field that's rarely present doesn't fit as a plain column. The Q1×Q4 intersection is where materialisation earns its keep.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — every materialised column costs some write-time CPU (evaluate the expression on insert/update) and some storage (the extracted value stored alongside the JSON). The trade-off is query-time speedup on the hot path; if the query fires 1000 times a day and the write fires 100 times, the trade almost always pays off.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — JSON&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;JSON extraction and reshape problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/json" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — parsing&lt;/span&gt;
&lt;strong&gt;Parsing drills&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/parsing" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  2. JSON_VALUE / JSON_EXTRACT — path expressions
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;json_value sql&lt;/code&gt; returns a scalar, &lt;code&gt;json_extract sql&lt;/code&gt; returns JSON — the same JSONPath grammar wraps them, but the dialect dresses the wrapper differently
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;every JSON-in-SQL path expression is a JSONPath (&lt;code&gt;$.a.b.c&lt;/code&gt;) wrapped in a dialect-specific function that returns either a scalar SQL type (&lt;code&gt;JSON_VALUE&lt;/code&gt;, &lt;code&gt;-&amp;gt;&amp;gt;&lt;/code&gt;) or a JSON sub-tree (&lt;code&gt;JSON_QUERY&lt;/code&gt;, &lt;code&gt;-&amp;gt;&lt;/code&gt;), and the interviewer expects you to name both halves of the split without prompting&lt;/strong&gt;. Once you say "scalar vs sub-tree, JSONPath vs pointer, arrow vs function," the path-expression interview surface is a lookup.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fimv1d1pk5g126lw1tss1.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fimv1d1pk5g126lw1tss1.jpeg" alt="Visual dialect-matrix of JSON path expressions — six dialect columns (Postgres, MySQL, SQL Server, Snowflake, BigQuery, Oracle); each cell shows the canonical path extraction syntax for the same nested lookup; a small annotation about JSON_VALUE vs JSON_QUERY vs JSON_EXTRACT; on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JSONPath grammar refresher.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;$&lt;/code&gt; — root of the JSON document.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.key&lt;/code&gt; — child by name (dot notation). Fails if the key contains hyphens, spaces, or reserved words — use bracket notation instead.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;["key-with-hyphen"]&lt;/code&gt; — bracket notation. Always works, always safe.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;[i]&lt;/code&gt; — child by array index (0-based).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;[*]&lt;/code&gt; — wildcard over an array (every element).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;..key&lt;/code&gt; — recursive descent (find &lt;code&gt;key&lt;/code&gt; anywhere in the sub-tree). Not supported in SQL Server.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;[?(@.total &amp;gt; 100)]&lt;/code&gt; — filter expression (SQL/JSON standard; Postgres and MySQL 8 support it).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Dot vs bracket notation.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use dot when the key is a plain identifier: &lt;code&gt;$.user.id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Use bracket when the key has a hyphen, space, or reserved word: &lt;code&gt;$["user-id"]&lt;/code&gt;, &lt;code&gt;$["order date"]&lt;/code&gt;, &lt;code&gt;$["$type"]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Bracket notation works everywhere dot notation works, so a code generator that emits pure bracket notation is bulletproof.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Postgres arrow operators.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-&amp;gt;&lt;/code&gt; — child by name, returns &lt;code&gt;jsonb&lt;/code&gt;. &lt;code&gt;payload -&amp;gt; 'user'&lt;/code&gt; returns the &lt;code&gt;user&lt;/code&gt; sub-tree as &lt;code&gt;jsonb&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-&amp;gt;&amp;gt;&lt;/code&gt; — child by name, returns &lt;code&gt;text&lt;/code&gt;. &lt;code&gt;payload -&amp;gt;&amp;gt; 'user_id'&lt;/code&gt; returns the value as a SQL &lt;code&gt;text&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;#&amp;gt;&lt;/code&gt; — path by text-array, returns &lt;code&gt;jsonb&lt;/code&gt;. &lt;code&gt;payload #&amp;gt; '{user, id}'&lt;/code&gt; navigates to &lt;code&gt;user.id&lt;/code&gt; and returns &lt;code&gt;jsonb&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;#&amp;gt;&amp;gt;&lt;/code&gt; — path by text-array, returns &lt;code&gt;text&lt;/code&gt;. Same navigation but returns &lt;code&gt;text&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;These are shortcuts over &lt;code&gt;jsonb_extract_path&lt;/code&gt; and &lt;code&gt;jsonb_extract_path_text&lt;/code&gt; and are more concise for common queries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Postgres SQL/JSON path API (Postgres 12+).&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;jsonb_path_query(payload, '$.orders[*].total')&lt;/code&gt; — set-returning; one row per match.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;jsonb_path_query_first(payload, '$.user_id')&lt;/code&gt; — scalar; first match or NULL.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;jsonb_path_exists(payload, '$.user_id')&lt;/code&gt; — boolean; true if any match.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;jsonb_path_match(payload, '$.total &amp;gt; 100')&lt;/code&gt; — boolean; true if the SQL/JSON predicate matches.&lt;/li&gt;
&lt;li&gt;These functions accept the full SQL/JSON path grammar including filter expressions and recursive descent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;MySQL 8 JSON_EXTRACT + shortcuts.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;JSON_EXTRACT(payload, '$.user.id')&lt;/code&gt; — returns a JSON value.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;payload -&amp;gt; '$.user.id'&lt;/code&gt; — shortcut for &lt;code&gt;JSON_EXTRACT&lt;/code&gt; (returns JSON, keeps quotes).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;payload -&amp;gt;&amp;gt; '$.user.id'&lt;/code&gt; — shortcut for &lt;code&gt;JSON_UNQUOTE(JSON_EXTRACT(...))&lt;/code&gt; (returns text, strips quotes).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;JSON_VALUE(payload, '$.user.id')&lt;/code&gt; — added in MySQL 8.0.21; returns a scalar with optional type coercion (&lt;code&gt;RETURNING INT&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;JSON_UNQUOTE(x)&lt;/code&gt; — strips the JSON string quotes; often paired with &lt;code&gt;JSON_EXTRACT&lt;/code&gt; when you need a plain SQL string.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;SQL Server JSON_VALUE vs JSON_QUERY.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;JSON_VALUE(payload, '$.user.id')&lt;/code&gt; — scalar. Returns &lt;code&gt;NVARCHAR(4000)&lt;/code&gt; by default; use the optional &lt;code&gt;RETURNING&lt;/code&gt; clause (SQL Server 2022+) for typed return.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;JSON_QUERY(payload, '$.user')&lt;/code&gt; — sub-tree. Returns &lt;code&gt;NVARCHAR(MAX)&lt;/code&gt; containing valid JSON.&lt;/li&gt;
&lt;li&gt;Lax mode (default) — path errors return &lt;code&gt;NULL&lt;/code&gt;. Strict mode (&lt;code&gt;'strict $.user.id'&lt;/code&gt;) — path errors throw.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ISJSON(x)&lt;/code&gt; — returns 1 if &lt;code&gt;x&lt;/code&gt; is valid JSON; use it in &lt;code&gt;CHECK&lt;/code&gt; constraints to validate a text column stores valid JSON.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;OPENJSON(payload, '$.orders')&lt;/code&gt; — flattens an array to rows (see the JSON_TABLE section).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Snowflake VARIANT access.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;payload:user_id&lt;/code&gt; — colon-notation child access. Returns &lt;code&gt;VARIANT&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;payload:user.id&lt;/code&gt; — chained dot after colon. Same as &lt;code&gt;payload:user:id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;payload:orders[0].total&lt;/code&gt; — bracket for index, dot for child.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;payload:user_id::string&lt;/code&gt; — cast operator forces the return type.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET_PATH(payload, 'user.id')&lt;/code&gt; — function form, useful for programmatic access.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET(payload, 'user_id')&lt;/code&gt; and &lt;code&gt;TRY_PARSE_JSON(text)&lt;/code&gt; — helpers for defensive access and casting.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;BigQuery JSON functions.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;JSON_VALUE(payload, '$.user.id')&lt;/code&gt; — scalar; returns &lt;code&gt;STRING&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;JSON_QUERY(payload, '$.user')&lt;/code&gt; — sub-tree; returns &lt;code&gt;STRING&lt;/code&gt; (JSON text) if &lt;code&gt;payload&lt;/code&gt; is &lt;code&gt;STRING&lt;/code&gt;, or &lt;code&gt;JSON&lt;/code&gt; if &lt;code&gt;payload&lt;/code&gt; is the native type.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;JSON_EXTRACT_SCALAR(payload, '$.user.id')&lt;/code&gt; — legacy alias for &lt;code&gt;JSON_VALUE&lt;/code&gt; on &lt;code&gt;STRING&lt;/code&gt;-typed columns.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;JSON_EXTRACT(payload, '$.user')&lt;/code&gt; — legacy alias for &lt;code&gt;JSON_QUERY&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Dot notation on native JSON: &lt;code&gt;payload.user.id&lt;/code&gt; — reads elegantly, only works on native &lt;code&gt;JSON&lt;/code&gt; (not &lt;code&gt;STRING&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Oracle JSON access.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;JSON_VALUE(payload, '$.user.id')&lt;/code&gt; — scalar with &lt;code&gt;RETURNING VARCHAR2(100)&lt;/code&gt; clause.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;JSON_QUERY(payload, '$.user')&lt;/code&gt; — sub-tree.&lt;/li&gt;
&lt;li&gt;Dot notation on &lt;code&gt;JSON&lt;/code&gt; columns: &lt;code&gt;t.payload.user.id&lt;/code&gt; (Oracle 12c+).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;JSON_EXISTS(payload, '$.user.id')&lt;/code&gt; — boolean.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;JSON_TABLE(payload, '$.orders[*]' COLUMNS ...)&lt;/code&gt; — the ANSI flattening operator; Oracle was the first to ship it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When the extraction returns &lt;code&gt;NULL&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Path doesn't match — the key doesn't exist. Every dialect returns &lt;code&gt;NULL&lt;/code&gt; in lax mode.&lt;/li&gt;
&lt;li&gt;Path matches but the value is &lt;code&gt;null&lt;/code&gt; (the JSON literal null). Postgres returns SQL &lt;code&gt;NULL&lt;/code&gt;; SQL Server returns SQL &lt;code&gt;NULL&lt;/code&gt;; Snowflake returns SQL &lt;code&gt;NULL&lt;/code&gt; after &lt;code&gt;::string&lt;/code&gt; cast; MySQL returns the JSON null which is not equal to SQL NULL (a common &lt;code&gt;mysql json&lt;/code&gt; gotcha).&lt;/li&gt;
&lt;li&gt;Path matches but the value is a sub-tree (object or array) and you asked for a scalar. SQL Server returns &lt;code&gt;NULL&lt;/code&gt; in lax mode; strict mode throws.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Type coercion after extraction.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every dialect returns the extracted value as text by default. To cast to a numeric type, use &lt;code&gt;CAST(... AS INT)&lt;/code&gt; in ANSI-flavoured dialects or &lt;code&gt;::int&lt;/code&gt; in Postgres.&lt;/li&gt;
&lt;li&gt;Snowflake requires an explicit &lt;code&gt;::type&lt;/code&gt; cast to leave the &lt;code&gt;VARIANT&lt;/code&gt; universe.&lt;/li&gt;
&lt;li&gt;BigQuery's &lt;code&gt;JSON_VALUE&lt;/code&gt; returns &lt;code&gt;STRING&lt;/code&gt;; use &lt;code&gt;SAFE_CAST(JSON_VALUE(...) AS INT64)&lt;/code&gt; for safe coercion.&lt;/li&gt;
&lt;li&gt;MySQL 8's &lt;code&gt;JSON_VALUE&lt;/code&gt; accepts a &lt;code&gt;RETURNING&lt;/code&gt; clause: &lt;code&gt;JSON_VALUE(payload, '$.total' RETURNING SIGNED)&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — Postgres arrow chain vs SQL/JSON path
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Postgres offers two paths for extraction: the arrow chain (&lt;code&gt;payload-&amp;gt;'user'-&amp;gt;&amp;gt;'id'&lt;/code&gt;) and the SQL/JSON path (&lt;code&gt;jsonb_path_query_first(payload, '$.user.id')&lt;/code&gt;). Both produce the same result. The arrow chain is more concise; the SQL/JSON path supports filter expressions and recursive descent. Knowing when to reach for each is a senior signal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given an &lt;code&gt;events&lt;/code&gt; table with &lt;code&gt;payload jsonb&lt;/code&gt;, write two equivalent Postgres queries — one using arrow chains, one using &lt;code&gt;jsonb_path_query_first&lt;/code&gt; — that project &lt;code&gt;user_id&lt;/code&gt;, &lt;code&gt;session_id&lt;/code&gt;, and the total of the first order. Explain when the SQL/JSON path form wins.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;payload&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;{"user_id": "u1", "session_id": "s1", "orders": [{"id": 1, "total": 10}]}&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;{"user_id": "u2", "session_id": "s2", "orders": [{"id": 2, "total": 30}]}&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Arrow-chain form&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'user_id'&lt;/span&gt;                            &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'session_id'&lt;/span&gt;                         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'orders'&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'total'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;      &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;first_total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- SQL/JSON path form&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;jsonb_path_query_first&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.user_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="o"&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;jsonb_path_query_first&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.session_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="o"&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jsonb_path_query_first&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.orders[0].total'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;first_total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;payload -&amp;gt;&amp;gt; 'user_id'&lt;/code&gt; returns the text value of the &lt;code&gt;user_id&lt;/code&gt; key. Concise and readable — the canonical Postgres extraction for a top-level scalar.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;payload -&amp;gt; 'orders' -&amp;gt; 0 -&amp;gt;&amp;gt; 'total'&lt;/code&gt; chains: extract &lt;code&gt;orders&lt;/code&gt; (jsonb), index into position 0 (jsonb), extract &lt;code&gt;total&lt;/code&gt; (text). The &lt;code&gt;::int&lt;/code&gt; cast forces numeric.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;jsonb_path_query_first(payload, '$.orders[0].total')&lt;/code&gt; returns the first match as &lt;code&gt;jsonb&lt;/code&gt;. To convert to SQL text, chain &lt;code&gt;#&amp;gt;&amp;gt; '{}'&lt;/code&gt; — a bit awkward but ANSI-flavoured.&lt;/li&gt;
&lt;li&gt;The SQL/JSON path form wins for filter expressions: &lt;code&gt;jsonb_path_query(payload, '$.orders[*] ? (@.total &amp;gt; 20)')&lt;/code&gt; returns every order with &lt;code&gt;total &amp;gt; 20&lt;/code&gt; — impossible in a pure arrow chain.&lt;/li&gt;
&lt;li&gt;The SQL/JSON path form also wins for recursive descent: &lt;code&gt;jsonb_path_query(payload, '$..id')&lt;/code&gt; returns every &lt;code&gt;id&lt;/code&gt; anywhere in the sub-tree — again impossible in a pure arrow chain.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;session_id&lt;/th&gt;
&lt;th&gt;first_total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;s1&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;s2&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Use arrow chains for simple top-level or single-branch extractions. Reach for &lt;code&gt;jsonb_path_query&lt;/code&gt; when you need filter predicates, recursive descent, or want the full SQL/JSON grammar (Postgres 12+).&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — MySQL JSON_EXTRACT with &lt;code&gt;JSON_UNQUOTE&lt;/code&gt; and &lt;code&gt;-&amp;gt;&amp;gt;&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; MySQL's most-asked JSON gotcha: &lt;code&gt;JSON_EXTRACT(payload, '$.user_id')&lt;/code&gt; returns &lt;code&gt;"u1"&lt;/code&gt; — a JSON string with the quotes still on. To get the SQL string &lt;code&gt;u1&lt;/code&gt;, you either wrap with &lt;code&gt;JSON_UNQUOTE&lt;/code&gt; or use the &lt;code&gt;-&amp;gt;&amp;gt;&lt;/code&gt; shortcut. Interviewers love this gotcha because it separates casual from careful MySQL users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given a MySQL 8 &lt;code&gt;events&lt;/code&gt; table with &lt;code&gt;payload JSON&lt;/code&gt;, write two equivalent queries that project &lt;code&gt;user_id&lt;/code&gt; as an unquoted SQL string. Show how a naive &lt;code&gt;JSON_EXTRACT&lt;/code&gt; breaks a downstream string equality.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;payload&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;{"user_id": "u1"}&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;{"user_id": "u2"}&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- BROKEN — quotes leak into the projected value&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;JSON_EXTRACT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.user_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Result: user_id = "u1" (a 4-char string with quotes)&lt;/span&gt;

&lt;span class="c1"&gt;-- FIXED (long form) — wrap with JSON_UNQUOTE&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;JSON_UNQUOTE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JSON_EXTRACT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.user_id'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- FIXED (shortcut) — the -&amp;gt;&amp;gt; operator is JSON_UNQUOTE(JSON_EXTRACT(...))&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'$.user_id'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- ALT — MySQL 8.0.21+ ships JSON_VALUE with optional RETURNING&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.user_id'&lt;/span&gt; &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;JSON_EXTRACT&lt;/code&gt; returns a JSON value — for a string, that means the string with its quotes. Comparing &lt;code&gt;JSON_EXTRACT(payload, '$.user_id') = 'u1'&lt;/code&gt; fails silently because &lt;code&gt;"u1"&lt;/code&gt; (with quotes) is not equal to &lt;code&gt;u1&lt;/code&gt; (without).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;JSON_UNQUOTE(JSON_EXTRACT(payload, '$.user_id'))&lt;/code&gt; strips the quotes and returns a plain SQL string. This is the canonical fix.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;payload-&amp;gt;&amp;gt;'$.user_id'&lt;/code&gt; is the syntactic shortcut for &lt;code&gt;JSON_UNQUOTE(JSON_EXTRACT(payload, '$.user_id'))&lt;/code&gt;. Reads cleaner and is the community-preferred form.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;JSON_VALUE(payload, '$.user_id')&lt;/code&gt; (MySQL 8.0.21+) is a modern alternative that returns a scalar. The optional &lt;code&gt;RETURNING VARCHAR(64)&lt;/code&gt; clause forces the return type — great when you know the target column type upfront.&lt;/li&gt;
&lt;li&gt;In every case, always use the unquoted form when the extracted value is a &lt;code&gt;WHERE&lt;/code&gt;, &lt;code&gt;GROUP BY&lt;/code&gt;, &lt;code&gt;JOIN&lt;/code&gt;, or &lt;code&gt;ORDER BY&lt;/code&gt; target. The quote-leak bug is silent — the query returns 0 rows instead of erroring, which is the worst kind of bug.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output (fixed variants).&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; In MySQL, always reach for &lt;code&gt;-&amp;gt;&amp;gt;&lt;/code&gt; (or &lt;code&gt;JSON_UNQUOTE(JSON_EXTRACT(...))&lt;/code&gt;) when the extracted value will be compared, grouped, joined, or ordered. Save &lt;code&gt;JSON_EXTRACT&lt;/code&gt; alone for JSON sub-tree extraction where the quotes belong.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — Snowflake VARIANT chained access with casts
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Snowflake's &lt;code&gt;VARIANT&lt;/code&gt; type accepts colon notation for key access and returns another &lt;code&gt;VARIANT&lt;/code&gt; — leaving the value untyped until you cast. The idiomatic Snowflake style is &lt;code&gt;payload:user_id::string&lt;/code&gt; — colon for access, &lt;code&gt;::&lt;/code&gt; for cast. Missing the cast is one of the top three &lt;code&gt;snowflake variant&lt;/code&gt; interview corrections.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given a Snowflake &lt;code&gt;events(id, payload VARIANT)&lt;/code&gt; table, write a query that projects &lt;code&gt;user_id&lt;/code&gt; as &lt;code&gt;STRING&lt;/code&gt;, &lt;code&gt;total&lt;/code&gt; as &lt;code&gt;NUMBER&lt;/code&gt;, and &lt;code&gt;is_paid&lt;/code&gt; as &lt;code&gt;BOOLEAN&lt;/code&gt;. Explain what happens if you skip the casts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;payload&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;{"user_id": "u1", "total": 100, "is_paid": true}&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;{"user_id": "u2", "total": 250, "is_paid": false}&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;is_paid&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;boolean&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;is_paid&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Alternative — GET_PATH is the function form&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;GET_PATH&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'user_id'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;GET_PATH&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'total'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;payload:user_id&lt;/code&gt; returns a &lt;code&gt;VARIANT&lt;/code&gt; whose scalar content is the string &lt;code&gt;"u1"&lt;/code&gt;. Without a cast, downstream string equality (&lt;code&gt;WHERE user_id = 'u1'&lt;/code&gt;) still works because Snowflake auto-coerces, but the plan may be slower and column type semantics can surprise you.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;payload:user_id::string&lt;/code&gt; casts to &lt;code&gt;VARCHAR&lt;/code&gt; — the type shows up in the plan, downstream tools see a string column, and the plan is faster because the cast can be pushed down.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;payload:total::number&lt;/code&gt; casts to &lt;code&gt;NUMBER&lt;/code&gt; — necessary for arithmetic or numeric aggregation. Skipping the cast can produce silent JSON-to-string coercion that breaks &lt;code&gt;SUM(total)&lt;/code&gt; in subtle ways.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET_PATH(payload, 'user_id')&lt;/code&gt; is the function form of the colon operator. Useful for programmatic path construction; verbose for hand-written queries.&lt;/li&gt;
&lt;li&gt;Every column with a materially-typed downstream consumer should be cast explicitly. The general Snowflake rule of thumb: cast at the earliest project, not the last.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;total&lt;/th&gt;
&lt;th&gt;is_paid&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;250&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; In Snowflake, always cast the extracted &lt;code&gt;VARIANT&lt;/code&gt; to a concrete SQL type at the earliest projection. The &lt;code&gt;::string&lt;/code&gt;, &lt;code&gt;::number&lt;/code&gt;, &lt;code&gt;::boolean&lt;/code&gt; casts pay for themselves in plan quality and downstream compatibility.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on portable path extraction
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "You're building a dbt package that must run on Postgres, Snowflake, BigQuery, and SQL Server. Write a portable macro that extracts a top-level scalar key from a JSON column and returns it as a string. What's the mental split you use for the four dialects?"&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a dbt cross-dialect macro dispatch
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- macros/json_scalar.sql&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;macro&lt;/span&gt; &lt;span class="n"&gt;json_scalar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%-&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'postgres'&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'redshift'&lt;/span&gt; &lt;span class="o"&gt;-%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;({{&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'{{ key }}'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%-&lt;/span&gt; &lt;span class="n"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'snowflake'&lt;/span&gt; &lt;span class="o"&gt;-%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;({{&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="p"&gt;}}:{{&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="p"&gt;}}::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%-&lt;/span&gt; &lt;span class="n"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'bigquery'&lt;/span&gt; &lt;span class="o"&gt;-%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;({{&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="p"&gt;}},&lt;/span&gt; &lt;span class="s1"&gt;'$.{{ key }}'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%-&lt;/span&gt; &lt;span class="n"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'sqlserver'&lt;/span&gt; &lt;span class="o"&gt;-%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;({{&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="p"&gt;}},&lt;/span&gt; &lt;span class="s1"&gt;'$.{{ key }}'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%-&lt;/span&gt; &lt;span class="n"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'mysql'&lt;/span&gt; &lt;span class="o"&gt;-%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JSON_UNQUOTE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JSON_EXTRACT&lt;/span&gt;&lt;span class="p"&gt;({{&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="p"&gt;}},&lt;/span&gt; &lt;span class="s1"&gt;'$.{{ key }}'&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%-&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;-%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;exceptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raise_compiler_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'json_scalar not supported on '&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%-&lt;/span&gt; &lt;span class="n"&gt;endif&lt;/span&gt; &lt;span class="o"&gt;-%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;endmacro&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;-- usage in a model&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;json_scalar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'payload'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'user_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;json_scalar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'payload'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'session_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'events'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Adapter&lt;/th&gt;
&lt;th&gt;Rendered expression&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;postgres&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(payload-&amp;gt;&amp;gt;'user_id')&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;snowflake&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(payload:user_id::string)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bigquery&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(JSON_VALUE(payload, '$.user_id'))&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sqlserver&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(JSON_VALUE(payload, '$.user_id'))&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mysql&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(JSON_UNQUOTE(JSON_EXTRACT(payload, '$.user_id')))&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Once the macro is defined, every model that reads a JSON column calls &lt;code&gt;{{ json_scalar('payload', 'user_id') }}&lt;/code&gt; and the compiled SQL is dialect-correct without manual edits. The macro is the analytics-engineer's answer to the JSON dialect fracture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dialect&lt;/th&gt;
&lt;th&gt;Portability outcome&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;postgres&lt;/td&gt;
&lt;td&gt;Uses the arrow shortcut; identical plan to &lt;code&gt;jsonb_extract_path_text&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;snowflake&lt;/td&gt;
&lt;td&gt;Uses colon notation + cast; plan-eligible for VARIANT columnar prune&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bigquery&lt;/td&gt;
&lt;td&gt;Uses &lt;code&gt;JSON_VALUE&lt;/code&gt;; native on &lt;code&gt;JSON&lt;/code&gt; type, works on &lt;code&gt;STRING&lt;/code&gt; too&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sqlserver&lt;/td&gt;
&lt;td&gt;Uses &lt;code&gt;JSON_VALUE&lt;/code&gt;; scalar-only, returns NVARCHAR(4000) default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mysql&lt;/td&gt;
&lt;td&gt;Uses &lt;code&gt;JSON_UNQUOTE(JSON_EXTRACT(...))&lt;/code&gt;; safe for equality and GROUP BY&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Adapter-dispatch is the portable pattern&lt;/strong&gt;&lt;/strong&gt; — every warehouse has a different function for the same concept, but dbt's &lt;code&gt;target.type&lt;/code&gt; variable lets you pick the right one at compile time. The compiled SQL is dialect-native; only the source is portable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Compile-time dispatch beats runtime abstraction&lt;/strong&gt;&lt;/strong&gt; — you don't want a runtime CASE that picks the syntax; the query planner would see all branches. Compile-time dispatch produces one clean SQL statement per target.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Scalar-vs-sub-tree matters even in the macro&lt;/strong&gt;&lt;/strong&gt; — the macro name &lt;code&gt;json_scalar&lt;/code&gt; documents that the output is a scalar. A separate &lt;code&gt;json_query&lt;/code&gt; macro handles sub-tree extraction. Never mix the two.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Fallback error&lt;/strong&gt;&lt;/strong&gt; — the &lt;code&gt;else&lt;/code&gt; branch raises a compiler error for unsupported adapters. Better to fail loudly at compile time than to silently produce wrong SQL for an unknown adapter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — the macro is compile-time only; there is zero query-time overhead. Every dialect gets its native fast path. This is the analytics-engineer's answer to the JSON dialect fracture.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — JSON&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;JSON path extraction problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/json" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — string parsing&lt;/span&gt;
&lt;strong&gt;String parsing and extraction drills&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/string-parsing" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  3. JSONB in Postgres — index it or regret it
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;jsonb postgres&lt;/code&gt; is the binary, deduped, indexable JSON type — pair it with a GIN index on the containment operator or every query becomes a full scan
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;Postgres ships two JSON types (&lt;code&gt;json&lt;/code&gt; and &lt;code&gt;jsonb&lt;/code&gt;) with subtly different semantics; only &lt;code&gt;jsonb&lt;/code&gt; is indexable; the canonical index is a GIN with either &lt;code&gt;jsonb_ops&lt;/code&gt; (universal) or &lt;code&gt;jsonb_path_ops&lt;/code&gt; (containment-only, ~30% smaller); and the &lt;code&gt;@&amp;gt;&lt;/code&gt; containment operator is the JSON equivalent of &lt;code&gt;= ANY(...)&lt;/code&gt; that every senior data engineer knows cold&lt;/strong&gt;. Once you say "jsonb + GIN + &lt;code&gt;@&amp;gt;&lt;/code&gt; = fast JSON queries in Postgres," the interview surface collapses to a recipe list.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0byowv8ztanvolk3avgb.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0byowv8ztanvolk3avgb.jpeg" alt="Visual diagram of Postgres jsonb GIN indexing — left a jsonb vs json comparison card, right a GIN index diagram showing jsonb_ops vs jsonb_path_ops trade-offs; a containment @&gt; example card at the bottom; on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;jsonb&lt;/code&gt; vs &lt;code&gt;json&lt;/code&gt; in Postgres.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;json&lt;/code&gt; — raw text storage. Preserves whitespace, key order, and duplicates. No parsing on read (which is cheap on write, expensive on read). No indexing.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;jsonb&lt;/code&gt; — binary parsed storage. Keys are deduped, whitespace is dropped, key order is not preserved. Parsing happens once on insert; every read is cheap. Indexable via GIN.&lt;/li&gt;
&lt;li&gt;Storage: &lt;code&gt;jsonb&lt;/code&gt; is typically ~5-15% smaller than the equivalent &lt;code&gt;json&lt;/code&gt; (dedup + binary encoding beat text).&lt;/li&gt;
&lt;li&gt;Write cost: &lt;code&gt;jsonb&lt;/code&gt; is ~20-40% slower on insert because parsing happens up-front. Usually a great trade if the row is read many times.&lt;/li&gt;
&lt;li&gt;Query cost: &lt;code&gt;jsonb&lt;/code&gt; is dramatically faster on any read that navigates the sub-tree (arrow operators, &lt;code&gt;@&amp;gt;&lt;/code&gt;, &lt;code&gt;?&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When to use &lt;code&gt;json&lt;/code&gt; (rare).&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need to preserve key order or whitespace (RFC 7396 patches, canonical form for signing).&lt;/li&gt;
&lt;li&gt;You never query on the JSON content, only pass it through.&lt;/li&gt;
&lt;li&gt;You measured and confirmed the write cost dominates the workload.&lt;/li&gt;
&lt;li&gt;For 99% of cases, &lt;code&gt;jsonb&lt;/code&gt; is the right default.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;GIN operator classes.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;jsonb_ops&lt;/code&gt; — DEFAULT. Indexes every key AND every value. Supports &lt;code&gt;@&amp;gt;&lt;/code&gt;, &lt;code&gt;?&lt;/code&gt;, &lt;code&gt;?&amp;amp;&lt;/code&gt;, &lt;code&gt;?|&lt;/code&gt;, and the SQL/JSON path operators &lt;code&gt;@?&lt;/code&gt;, &lt;code&gt;@@&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;jsonb_path_ops&lt;/code&gt; — SMALLER. Indexes only &lt;code&gt;@&amp;gt;&lt;/code&gt; containment. Doesn't support &lt;code&gt;?&lt;/code&gt; existence. ~30% smaller and faster to build; picks the right choice for 90% of production containment workloads.&lt;/li&gt;
&lt;li&gt;Choose &lt;code&gt;jsonb_path_ops&lt;/code&gt; when your read pattern is 100% containment (&lt;code&gt;WHERE payload @&amp;gt; '{"status": "active"}'&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Choose &lt;code&gt;jsonb_ops&lt;/code&gt; when you also need key-existence queries (&lt;code&gt;WHERE payload ? 'user_id'&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;GIN index recipe.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Universal — supports @&amp;gt;, ?, ?&amp;amp;, ?|&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;ix_events_payload_gin&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;GIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Containment-only — smaller, faster to build&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;ix_events_payload_gin_path&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;GIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="n"&gt;jsonb_path_ops&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The &lt;code&gt;@&amp;gt;&lt;/code&gt; containment operator.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;payload @&amp;gt; '{"status": "active"}'&lt;/code&gt; — true if &lt;code&gt;payload&lt;/code&gt; contains the given JSON as a sub-object.&lt;/li&gt;
&lt;li&gt;Works recursively: &lt;code&gt;payload @&amp;gt; '{"user": {"id": "u1"}}'&lt;/code&gt; matches any payload with a &lt;code&gt;user&lt;/code&gt; sub-object containing &lt;code&gt;id: u1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Works for arrays: &lt;code&gt;payload @&amp;gt; '{"tags": ["premium"]}'&lt;/code&gt; matches any payload where &lt;code&gt;tags&lt;/code&gt; contains &lt;code&gt;premium&lt;/code&gt; (among other elements).&lt;/li&gt;
&lt;li&gt;GIN-indexable, unlike most JSON extraction predicates.&lt;/li&gt;
&lt;li&gt;The canonical &lt;code&gt;WHERE&lt;/code&gt; clause for JSON filtering in Postgres.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Existence operators (&lt;code&gt;jsonb_ops&lt;/code&gt; only).&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;payload ? 'user_id'&lt;/code&gt; — true if &lt;code&gt;payload&lt;/code&gt; has a top-level key &lt;code&gt;user_id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;payload ?&amp;amp; array['user_id','ts']&lt;/code&gt; — true if all listed keys exist.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;payload ?| array['a','b']&lt;/code&gt; — true if any listed key exists.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;SQL/JSON path operators (Postgres 12+).&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;payload @? '$.user.id'&lt;/code&gt; — true if any value matches the path (existence check).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;payload @@ '$.total &amp;gt; 100'&lt;/code&gt; — true if the SQL/JSON predicate matches.&lt;/li&gt;
&lt;li&gt;Both are GIN-indexable via &lt;code&gt;jsonb_ops&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;jsonb_path_query&lt;/code&gt; for extraction with predicates.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;jsonb_path_query(payload, '$.orders[*] ? (@.total &amp;gt; 100)')&lt;/code&gt; — returns every order with &lt;code&gt;total &amp;gt; 100&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Set-returning; useful in &lt;code&gt;LATERAL&lt;/code&gt; joins for row expansion.&lt;/li&gt;
&lt;li&gt;Predicate expressions inside &lt;code&gt;? (...)&lt;/code&gt; — the SQL/JSON standard filter form.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;jsonb_set&lt;/code&gt; and &lt;code&gt;||&lt;/code&gt; for updates.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;UPDATE events SET payload = jsonb_set(payload, '{status}', '"paid"') WHERE id = 1;&lt;/code&gt; — set a nested key.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;UPDATE events SET payload = payload || '{"reviewed": true}';&lt;/code&gt; — merge two objects (right side wins on key collision).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;UPDATE events SET payload = payload - 'temp_field';&lt;/code&gt; — delete a top-level key.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;UPDATE events SET payload = payload #- '{user,temp}';&lt;/code&gt; — delete a nested key.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Functional expression indexes for a single hot path.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Index a single extracted value — great for one hot filter&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;ix_events_user_id&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'user_id'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Use this when you only ever filter on one key. Cheaper than GIN, faster to plan.&lt;/li&gt;
&lt;li&gt;The trade-off: only supports the exact expression indexed. &lt;code&gt;WHERE payload-&amp;gt;&amp;gt;'user_id' = 'u1'&lt;/code&gt; uses it; &lt;code&gt;WHERE payload @&amp;gt; '{"user_id": "u1"}'&lt;/code&gt; does not.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Generated column materialisation.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Materialise a stable field as a plain column&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
  &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;GENERATED&lt;/span&gt; &lt;span class="n"&gt;ALWAYS&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'user_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;STORED&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;ix_events_user_id&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;The generated column stays in sync with the JSON automatically.&lt;/li&gt;
&lt;li&gt;Query planner sees a plain column and picks the BTREE index without any expression magic.&lt;/li&gt;
&lt;li&gt;Best pattern when the extracted field is a hot join key, group-by key, or sort key.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When plain &lt;code&gt;TEXT&lt;/code&gt; beats &lt;code&gt;jsonb&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The JSON is never queried, only passed through.&lt;/li&gt;
&lt;li&gt;The write throughput is critical and the parse cost matters more than the read cost.&lt;/li&gt;
&lt;li&gt;You need bit-identical JSON preservation (whitespace, key order, duplicates).&lt;/li&gt;
&lt;li&gt;All three cases are rare; &lt;code&gt;jsonb&lt;/code&gt; is the safe default.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common &lt;code&gt;jsonb postgres&lt;/code&gt; interview probes.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"When would you use &lt;code&gt;json&lt;/code&gt; over &lt;code&gt;jsonb&lt;/code&gt;?" — almost never; only when key order or whitespace preservation matters.&lt;/li&gt;
&lt;li&gt;"Which GIN operator class supports the existence operator &lt;code&gt;?&lt;/code&gt;?" — &lt;code&gt;jsonb_ops&lt;/code&gt; (default); &lt;code&gt;jsonb_path_ops&lt;/code&gt; does not.&lt;/li&gt;
&lt;li&gt;"What's the containment operator, and why is it interesting?" — &lt;code&gt;@&amp;gt;&lt;/code&gt;; the JSON equivalent of &lt;code&gt;= ANY(...)&lt;/code&gt;, GIN-indexable, the fastest filter in Postgres JSON.&lt;/li&gt;
&lt;li&gt;"How would you index a single hot filter key without a GIN?" — a functional expression index on &lt;code&gt;(payload-&amp;gt;&amp;gt;'key')&lt;/code&gt; or a generated column with a BTREE.&lt;/li&gt;
&lt;li&gt;"What does &lt;code&gt;jsonb_set&lt;/code&gt; do that &lt;code&gt;||&lt;/code&gt; doesn't?" — &lt;code&gt;jsonb_set&lt;/code&gt; writes to a nested path; &lt;code&gt;||&lt;/code&gt; only merges top-level keys.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Worked example — GIN index + containment filter for event routing
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The most common Postgres JSON workload: an events table with 100M rows, filtered by an event-type-specific containment predicate (&lt;code&gt;WHERE payload @&amp;gt; '{"kind": "purchase"}'&lt;/code&gt;). Without a GIN, this is a full scan. With a GIN, it's milliseconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given &lt;code&gt;events(id, ts, payload jsonb)&lt;/code&gt; with 100M rows, write the DDL to add a GIN index on &lt;code&gt;payload&lt;/code&gt; and the query that filters to purchase events. Explain the plan difference before and after the index.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;ts&lt;/th&gt;
&lt;th&gt;payload&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2026-07-01&lt;/td&gt;
&lt;td&gt;{"kind": "click", "user_id": "u1"}&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2026-07-01&lt;/td&gt;
&lt;td&gt;{"kind": "purchase", "user_id": "u1", "total": 100}&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2026-07-01&lt;/td&gt;
&lt;td&gt;{"kind": "view", "user_id": "u2"}&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- DDL — add a containment-only GIN for smaller footprint&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;CONCURRENTLY&lt;/span&gt; &lt;span class="n"&gt;ix_events_payload_gin&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;GIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="n"&gt;jsonb_path_ops&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Query — filter to purchase events&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'user_id'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'total'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;@&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'{"kind": "purchase"}'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Before the index: Seq Scan on events (cost=0.00..3000000.00)&lt;/span&gt;
&lt;span class="c1"&gt;-- After the index:  Bitmap Heap Scan on events (cost=100.00..500.00)&lt;/span&gt;
&lt;span class="c1"&gt;--                     -&amp;gt; Bitmap Index Scan on ix_events_payload_gin (cost=0.00..100.00)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;CREATE INDEX CONCURRENTLY&lt;/code&gt; builds the GIN without blocking writes. On a 100M-row table, this takes minutes to hours; run it during low-load windows.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;jsonb_path_ops&lt;/code&gt; operator class is ~30% smaller than the default &lt;code&gt;jsonb_ops&lt;/code&gt; and supports only &lt;code&gt;@&amp;gt;&lt;/code&gt;. That's the right trade for this workload — we only filter with containment.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;payload @&amp;gt; '{"kind": "purchase"}'&lt;/code&gt; is the containment predicate. The GIN maps every JSON path fragment to the row ids that contain it; the planner uses a bitmap of matching rows.&lt;/li&gt;
&lt;li&gt;Plan before the index: sequential scan reads all 100M rows, evaluates the predicate on each. Cost is millions of tuple reads.&lt;/li&gt;
&lt;li&gt;Plan after the index: bitmap index scan produces a set of ~100k candidate row ids (assuming 0.1% are purchases), then a bitmap heap scan visits each. Cost is 3-4 orders of magnitude lower.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;ts&lt;/th&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2026-07-01&lt;/td&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For any Postgres JSON column that will be queried by containment, add a GIN with &lt;code&gt;jsonb_path_ops&lt;/code&gt; as the default. Upgrade to &lt;code&gt;jsonb_ops&lt;/code&gt; only when you also need existence (&lt;code&gt;?&lt;/code&gt;, &lt;code&gt;?&amp;amp;&lt;/code&gt;, &lt;code&gt;?|&lt;/code&gt;) operators.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — functional expression index for a single hot key
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; When the read pattern is 100% on one extracted key — say &lt;code&gt;WHERE payload-&amp;gt;&amp;gt;'user_id' = 'u1'&lt;/code&gt; — a full GIN is overkill. A functional expression index on the extraction is smaller and faster. Interviewers love this micro-optimisation because it separates "reads docs" from "reads plans."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given &lt;code&gt;events(id, payload jsonb)&lt;/code&gt; and a workload that only filters on &lt;code&gt;payload-&amp;gt;&amp;gt;'user_id'&lt;/code&gt;, write the DDL for a functional expression index and confirm it beats a GIN for this specific query.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;payload&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;{"user_id": "u1", "kind": "click"}&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;{"user_id": "u2", "kind": "purchase"}&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;{"user_id": "u1", "kind": "view"}&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Functional expression index on the extracted key&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;ix_events_user_id&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'user_id'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;-- Query — uses the functional index&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'kind'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'user_id'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'u1'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Plan: Index Scan using ix_events_user_id (cost=0.42..8.44 rows=2)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The functional expression index stores &lt;code&gt;payload-&amp;gt;&amp;gt;'user_id'&lt;/code&gt; for every row and points at the row id. It's a BTREE, so range scans and equality both work at BTREE speed.&lt;/li&gt;
&lt;li&gt;The index is much smaller than a GIN because it stores exactly one key per row (versus GIN's many entries per row for every JSON path fragment).&lt;/li&gt;
&lt;li&gt;The query planner recognises the exact expression &lt;code&gt;payload-&amp;gt;&amp;gt;'user_id'&lt;/code&gt; in the WHERE clause and picks the functional index automatically. If you write the WHERE clause differently (e.g., &lt;code&gt;payload @&amp;gt; '{"user_id": "u1"}'&lt;/code&gt;), the functional index does NOT match — a common gotcha.&lt;/li&gt;
&lt;li&gt;Trade-off: this index only supports the one hot expression. If your workload later adds a second predicate (&lt;code&gt;WHERE payload-&amp;gt;&amp;gt;'session_id' = 's1'&lt;/code&gt;), you'd need a second functional index, or fall back to a GIN.&lt;/li&gt;
&lt;li&gt;In practice, the choice comes down to workload cardinality. If you have 1-2 hot keys, use functional indexes. If you have 5+ query patterns, use a GIN.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;kind&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;click&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;view&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For a workload with one or two hot extraction keys, functional expression indexes beat GIN on size, build time, and plan quality. For a workload with many extraction patterns or containment queries, GIN wins.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — generated column + BTREE for a stable join key
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; When a JSON key is a stable join target (present in every row, drives the query planner's join order), materialising it as a generated column with a BTREE is the fastest option. This is Postgres 12+ and is the analytics-engineer's canonical answer for hot join keys.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given &lt;code&gt;events(id, payload jsonb)&lt;/code&gt; where &lt;code&gt;payload-&amp;gt;&amp;gt;'user_id'&lt;/code&gt; is a hot join key against &lt;code&gt;users(id)&lt;/code&gt;, write the DDL to materialise &lt;code&gt;user_id&lt;/code&gt; as a generated column with a BTREE. Show a JOIN query that benefits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;payload&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;{"user_id": "u1", "kind": "click"}&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;{"user_id": "u2", "kind": "purchase"}&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;{"user_id": "u1", "kind": "view"}&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Materialise the join key&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
  &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;GENERATED&lt;/span&gt; &lt;span class="n"&gt;ALWAYS&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'user_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;STORED&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;ix_events_user_id&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- JOIN — planner sees a plain column&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'kind'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;GENERATED ALWAYS AS (payload-&amp;gt;&amp;gt;'user_id') STORED&lt;/code&gt; creates a physical column that materialises the extraction. Every insert or update to &lt;code&gt;payload&lt;/code&gt; re-evaluates the expression and stores the result.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;STORED&lt;/code&gt; keyword is required (Postgres only supports &lt;code&gt;STORED&lt;/code&gt;, not &lt;code&gt;VIRTUAL&lt;/code&gt;). It pays a small write cost and gives you a full-fidelity column at query time.&lt;/li&gt;
&lt;li&gt;A plain BTREE index on &lt;code&gt;user_id&lt;/code&gt; behaves exactly like any BTREE. The planner sees a normal column and picks the index for equality, range, and sort operations without any expression magic.&lt;/li&gt;
&lt;li&gt;The JOIN &lt;code&gt;ON u.id = e.user_id&lt;/code&gt; is a plain equi-join on two indexed columns. The planner picks a hash join or nested-loop join depending on cardinality — either way, it's optimal.&lt;/li&gt;
&lt;li&gt;The trade-off is disk usage: the extracted value is stored twice (once in JSON, once in the generated column). For hot join keys, this is almost always worth it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;kind&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Ada&lt;/td&gt;
&lt;td&gt;click&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Bo&lt;/td&gt;
&lt;td&gt;purchase&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Ada&lt;/td&gt;
&lt;td&gt;view&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For any JSON key that's stable, always present, and on a hot join / group-by / sort path, materialise it as a &lt;code&gt;GENERATED ALWAYS AS (...) STORED&lt;/code&gt; column with a plain BTREE. This is the fastest pattern in Postgres 12+ and the analytics-engineer's default answer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on Postgres JSONB tuning
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "You've inherited a Postgres table &lt;code&gt;events(id, ts, payload jsonb)&lt;/code&gt; with 500M rows. The team has one query pattern: &lt;code&gt;WHERE payload @&amp;gt; '{"kind": "..."}' AND ts &amp;gt; NOW() - INTERVAL '7 days'&lt;/code&gt;. Walk me through your indexing plan and why."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a composite plan of BTREE + GIN
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Step 1 — BTREE on the time dimension (partition-eligible)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;ix_events_ts&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 2 — GIN on the JSON payload for containment&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;CONCURRENTLY&lt;/span&gt; &lt;span class="n"&gt;ix_events_payload_gin&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;GIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="n"&gt;jsonb_path_ops&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 3 — optional partial index for the hot event types&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;ix_events_purchase&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;@&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'{"kind": "purchase"}'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 4 — analyze so the planner has fresh stats&lt;/span&gt;
&lt;span class="k"&gt;ANALYZE&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;BTREE on &lt;code&gt;ts DESC&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Fast time-window pruning; enables merge/append/index scans&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;GIN(jsonb_path_ops) on payload&lt;/td&gt;
&lt;td&gt;Fast containment; planner combines with BTREE via BitmapAnd&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Partial BTREE on &lt;code&gt;ts DESC WHERE payload @&amp;gt; ...&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Ultra-fast hot-event scans; skips containment entirely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;ANALYZE&lt;/td&gt;
&lt;td&gt;Refreshes stats so the planner picks the right combination&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The composite plan uses BTREE for time and GIN for containment; the planner combines the two via a BitmapAnd. For the ultra-hot event kinds where 90% of queries hit, the partial BTREE eliminates the containment step entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query pattern&lt;/th&gt;
&lt;th&gt;Plan the planner picks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ts &amp;gt; NOW() - 7 days&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;BitmapAnd(BTREE(ts), GIN(payload)) — fastest general case&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;payload @&amp;gt; '{"kind":"purchase"}' AND ts &amp;gt; ...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Partial BTREE — skips containment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;payload @&amp;gt; '{"kind":"rare"}' AND ts &amp;gt; ...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;BitmapAnd — general case&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;BTREE on the time dimension&lt;/strong&gt;&lt;/strong&gt; — every OLTP + analytics workload with a &lt;code&gt;ts&lt;/code&gt; column benefits from a descending BTREE. The DESC ordering matches the common "latest N days" pattern; the planner picks it for range scans and merge joins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;GIN on the JSON payload&lt;/strong&gt;&lt;/strong&gt; — the &lt;code&gt;jsonb_path_ops&lt;/code&gt; operator class supports only &lt;code&gt;@&amp;gt;&lt;/code&gt;, but that's the entire containment surface. Storing exactly the containment fragments makes the index ~30% smaller and faster than the default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Partial indexes for hot filters&lt;/strong&gt;&lt;/strong&gt; — when 90% of queries hit one event kind, a partial index that indexes only those rows is dramatically smaller than a full index. The planner picks it automatically when the WHERE clause matches the partial's predicate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;BitmapAnd combines multiple indexes&lt;/strong&gt;&lt;/strong&gt; — Postgres's planner can intersect BTREE and GIN bitmaps to find rows that satisfy both predicates. This is why you don't need one giant multi-column index; two smaller indexes plus BitmapAnd is often faster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — the composite plan is O(log N) for the BTREE + O(log N) for the GIN. On a 500M row table, that's tens of thousands of index reads instead of 500M sequential scans — a 4-5 order-of-magnitude speedup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — JSON&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;JSONB indexing and containment drills&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/json" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — data manipulation&lt;/span&gt;
&lt;strong&gt;Data manipulation problems&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/data-manipulation" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  4. JSON_TABLE — flatten JSON to rows
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;json_table sql&lt;/code&gt; is the ANSI SQL/JSON operator that turns a nested array into rows — Snowflake reaches for &lt;code&gt;LATERAL FLATTEN&lt;/code&gt;, BigQuery for &lt;code&gt;UNNEST(JSON_QUERY_ARRAY(...))&lt;/code&gt;, Postgres for &lt;code&gt;jsonb_to_recordset&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;a nested JSON array cannot be joined against a relational query without a flattening operator that emits one row per array element, and every warehouse ships a different keyword for the same operation&lt;/strong&gt;. Once you say "ANSI = &lt;code&gt;JSON_TABLE&lt;/code&gt;, Snowflake = &lt;code&gt;LATERAL FLATTEN&lt;/code&gt;, BigQuery = &lt;code&gt;UNNEST(JSON_QUERY_ARRAY)&lt;/code&gt;, Postgres = &lt;code&gt;jsonb_array_elements&lt;/code&gt; / &lt;code&gt;jsonb_to_recordset&lt;/code&gt;," the flattening interview surface reduces to a syntax lookup.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fapuak3a9gkv6b238bxw9.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fapuak3a9gkv6b238bxw9.jpeg" alt="Visual diagram of JSON_TABLE flattening — left a JSON payload with a nested orders array, an arrow labelled JSON_TABLE / LATERAL FLATTEN / UNNEST, right a relational output table; a dialect equivalents matrix at the bottom (ANSI JSON_TABLE, Snowflake LATERAL FLATTEN, BigQuery UNNEST(JSON_QUERY_ARRAY), Postgres jsonb_to_recordset); on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ANSI &lt;code&gt;JSON_TABLE&lt;/code&gt; — the standard.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Shipped by Oracle (first, 12c), MySQL 8.0.4+, SQL Server 2022+, IBM Db2, and PostgreSQL 17+.&lt;/li&gt;
&lt;li&gt;Syntax: &lt;code&gt;JSON_TABLE(payload, '$.orders[*]' COLUMNS (id INT PATH '$.id', total DECIMAL(10,2) PATH '$.total')) AS orders&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Reads: "for each element of &lt;code&gt;$.orders[*]&lt;/code&gt;, emit a row whose &lt;code&gt;id&lt;/code&gt; column comes from &lt;code&gt;$.id&lt;/code&gt; and &lt;code&gt;total&lt;/code&gt; column comes from &lt;code&gt;$.total&lt;/code&gt; (relative to the array element)."&lt;/li&gt;
&lt;li&gt;Used in a &lt;code&gt;FROM&lt;/code&gt; clause with a &lt;code&gt;LATERAL&lt;/code&gt; semantic (implicit) — the flattening is per-row.&lt;/li&gt;
&lt;li&gt;Supports &lt;code&gt;NESTED PATH&lt;/code&gt; sub-blocks for deeper flattening (see below).&lt;/li&gt;
&lt;li&gt;Supports &lt;code&gt;ERROR / NULL / DEFAULT ... ON EMPTY&lt;/code&gt; and &lt;code&gt;ERROR / NULL / DEFAULT ... ON ERROR&lt;/code&gt; for missing/invalid values.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Snowflake &lt;code&gt;LATERAL FLATTEN&lt;/code&gt; — the flagship for VARIANT.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Syntax: &lt;code&gt;SELECT e.id, o.value:id::int AS order_id, o.value:total::number AS total FROM events e, LATERAL FLATTEN(input =&amp;gt; e.payload:orders) o&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FLATTEN&lt;/code&gt; returns a table with columns &lt;code&gt;SEQ&lt;/code&gt;, &lt;code&gt;KEY&lt;/code&gt;, &lt;code&gt;PATH&lt;/code&gt;, &lt;code&gt;INDEX&lt;/code&gt;, &lt;code&gt;VALUE&lt;/code&gt;, &lt;code&gt;THIS&lt;/code&gt;. The interesting one is &lt;code&gt;VALUE&lt;/code&gt; (the element).&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;OUTER =&amp;gt; true&lt;/code&gt; to preserve rows where the flattened array is empty or NULL (like a LEFT JOIN).&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;RECURSIVE =&amp;gt; true&lt;/code&gt; to recurse into nested objects/arrays automatically.&lt;/li&gt;
&lt;li&gt;Great for semi-structured Snowflake VARIANT columns; the canonical Snowflake pattern.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;BigQuery &lt;code&gt;UNNEST(JSON_QUERY_ARRAY(...))&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Syntax: &lt;code&gt;SELECT e.id, JSON_VALUE(o, '$.id') AS order_id, JSON_VALUE(o, '$.total') AS total FROM events e, UNNEST(JSON_QUERY_ARRAY(e.payload, '$.orders')) AS o&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;JSON_QUERY_ARRAY&lt;/code&gt; returns an &lt;code&gt;ARRAY&amp;lt;STRING&amp;gt;&lt;/code&gt; (or &lt;code&gt;ARRAY&amp;lt;JSON&amp;gt;&lt;/code&gt; for native JSON) of the array elements.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;UNNEST&lt;/code&gt; flattens the array to rows in a &lt;code&gt;FROM&lt;/code&gt; clause.&lt;/li&gt;
&lt;li&gt;Per-element extraction still uses &lt;code&gt;JSON_VALUE&lt;/code&gt; / &lt;code&gt;JSON_QUERY&lt;/code&gt; on the unnested element.&lt;/li&gt;
&lt;li&gt;BigQuery also supports &lt;code&gt;JSON_EXTRACT_ARRAY&lt;/code&gt; as a legacy alias for &lt;code&gt;JSON_QUERY_ARRAY&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Postgres flattening options.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;jsonb_array_elements(payload -&amp;gt; 'orders')&lt;/code&gt; — set-returning; emits one row per array element, each as &lt;code&gt;jsonb&lt;/code&gt;. Used in a &lt;code&gt;LATERAL&lt;/code&gt; join.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;jsonb_to_recordset(payload -&amp;gt; 'orders')&lt;/code&gt; — set-returning; emits typed rows given a column list. &lt;code&gt;SELECT * FROM jsonb_to_recordset(payload -&amp;gt; 'orders') AS o(id int, total numeric)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;jsonb_array_elements_text(payload -&amp;gt; 'tags')&lt;/code&gt; — for string arrays; emits text.&lt;/li&gt;
&lt;li&gt;Postgres 17+ ships full ANSI &lt;code&gt;JSON_TABLE&lt;/code&gt; on &lt;code&gt;jsonb&lt;/code&gt; columns.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Nested arrays — &lt;code&gt;NESTED PATH&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ANSI &lt;code&gt;JSON_TABLE&lt;/code&gt; supports a &lt;code&gt;NESTED PATH '$.sub_array[*]' COLUMNS (...)&lt;/code&gt; sub-block for deeper flattening.&lt;/li&gt;
&lt;li&gt;Reads: "for each element of the outer array, also flatten the inner array named &lt;code&gt;sub_array&lt;/code&gt;, and emit one row per (outer, inner) pair."&lt;/li&gt;
&lt;li&gt;Great for hierarchical payloads like &lt;code&gt;{"user": "u1", "sessions": [{"session_id": "s1", "events": [{"kind": "click"}, {"kind": "view"}]}]}&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Null handling clauses.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;NULL ON EMPTY&lt;/code&gt; — the default; missing paths produce &lt;code&gt;NULL&lt;/code&gt; values in the output.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ERROR ON EMPTY&lt;/code&gt; — throw an error when the path doesn't match. Useful in strict-mode ETLs where a missing key is a bug.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DEFAULT 'x' ON EMPTY&lt;/code&gt; — substitute a default value.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ERROR ON ERROR&lt;/code&gt; — throw if the extracted value can't be coerced to the declared column type. Default is &lt;code&gt;NULL ON ERROR&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When to flatten vs when to keep in JSON.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Flatten when the array elements feed downstream joins, aggregates, or filters that would benefit from a relational shape.&lt;/li&gt;
&lt;li&gt;Keep in JSON when the array is a read-only projection that ships back to the client.&lt;/li&gt;
&lt;li&gt;Never flatten a large array eagerly if the query only needs a few elements — use &lt;code&gt;JSON_EXTRACT&lt;/code&gt; or &lt;code&gt;jsonb_path_query&lt;/code&gt; with a predicate to pull only what you need.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common &lt;code&gt;json_table sql&lt;/code&gt; interview probes.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Which dialects ship ANSI &lt;code&gt;JSON_TABLE&lt;/code&gt;?" — Oracle, MySQL 8.0.4+, SQL Server 2022+, PostgreSQL 17+.&lt;/li&gt;
&lt;li&gt;"What's the Snowflake equivalent?" — &lt;code&gt;LATERAL FLATTEN(input =&amp;gt; col)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;"What's the BigQuery equivalent?" — &lt;code&gt;UNNEST(JSON_QUERY_ARRAY(col, '$.path'))&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;"How would you handle a doubly-nested array?" — &lt;code&gt;NESTED PATH&lt;/code&gt; sub-block in ANSI; chained &lt;code&gt;LATERAL FLATTEN&lt;/code&gt; in Snowflake; chained &lt;code&gt;UNNEST&lt;/code&gt; in BigQuery; chained &lt;code&gt;LATERAL jsonb_array_elements&lt;/code&gt; in Postgres.&lt;/li&gt;
&lt;li&gt;"What's the difference between &lt;code&gt;JSON_TABLE&lt;/code&gt; and &lt;code&gt;OPENJSON&lt;/code&gt;?" — SQL Server ships both; &lt;code&gt;OPENJSON&lt;/code&gt; is Microsoft's older non-standard flattener, &lt;code&gt;JSON_TABLE&lt;/code&gt; is the ANSI one (2022+). Prefer &lt;code&gt;JSON_TABLE&lt;/code&gt; on new work.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — SQL Server JSON_TABLE for a nested orders array
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The canonical ANSI flattening ask: given an events table where each row has a &lt;code&gt;payload&lt;/code&gt; with a nested &lt;code&gt;orders&lt;/code&gt; array, produce one row per (event, order) pair with typed columns. SQL Server 2022+ ships &lt;code&gt;JSON_TABLE&lt;/code&gt;; older versions use &lt;code&gt;OPENJSON&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given &lt;code&gt;events(id, payload NVARCHAR(MAX))&lt;/code&gt; where &lt;code&gt;payload&lt;/code&gt; contains &lt;code&gt;{"user": "u1", "orders": [{"id": 1, "total": 10}, {"id": 2, "total": 25}]}&lt;/code&gt;, write a SQL Server 2022 &lt;code&gt;JSON_TABLE&lt;/code&gt; that produces &lt;code&gt;(event_id, user, order_id, total)&lt;/code&gt;. Explain the &lt;code&gt;PATH&lt;/code&gt; clause and the &lt;code&gt;COLUMNS&lt;/code&gt; list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;payload&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;{"user": "u1", "orders": [{"id": 1, "total": 10}, {"id": 2, "total": 25}]}&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;td&gt;{"user": "u2", "orders": [{"id": 3, "total": 40}]}&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;      &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.user'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
&lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="n"&gt;APPLY&lt;/span&gt; &lt;span class="n"&gt;JSON_TABLE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'$.orders[*]'&lt;/span&gt;
  &lt;span class="n"&gt;COLUMNS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;           &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt;    &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.total'&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;JSON_TABLE(e.payload, '$.orders[*]' COLUMNS (...))&lt;/code&gt; reads: "for each element of &lt;code&gt;$.orders[*]&lt;/code&gt; inside &lt;code&gt;e.payload&lt;/code&gt;, emit a row with the declared columns."&lt;/li&gt;
&lt;li&gt;The row-generating path &lt;code&gt;$.orders[*]&lt;/code&gt; selects every element of the &lt;code&gt;orders&lt;/code&gt; array. Each element becomes one output row.&lt;/li&gt;
&lt;li&gt;Inside &lt;code&gt;COLUMNS (...)&lt;/code&gt;, each column has a name, a SQL type, and a &lt;code&gt;PATH&lt;/code&gt; relative to the current array element. &lt;code&gt;order_id INT PATH '$.id'&lt;/code&gt; pulls &lt;code&gt;.id&lt;/code&gt; from the element, coerces to &lt;code&gt;INT&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CROSS APPLY&lt;/code&gt; (SQL Server keyword) makes &lt;code&gt;JSON_TABLE&lt;/code&gt; behave like a per-row function — for each event, the flattener runs against that event's payload.&lt;/li&gt;
&lt;li&gt;The outer &lt;code&gt;SELECT&lt;/code&gt; projects both the event columns (via &lt;code&gt;e.&lt;/code&gt; alias) and the flattened columns (via &lt;code&gt;o.&lt;/code&gt; alias). The result is one row per (event, order) pair.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;event_id&lt;/th&gt;
&lt;th&gt;user&lt;/th&gt;
&lt;th&gt;order_id&lt;/th&gt;
&lt;th&gt;total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;10.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;25.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;40.00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For any SQL Server 2022+ flattening task, prefer &lt;code&gt;JSON_TABLE&lt;/code&gt; over &lt;code&gt;OPENJSON&lt;/code&gt; — it's ANSI, portable to Oracle / MySQL / PostgreSQL 17+, and reads more clearly. Keep &lt;code&gt;OPENJSON&lt;/code&gt; for legacy SQL Server 2017-2019 codebases.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — Snowflake LATERAL FLATTEN for VARIANT arrays
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The canonical Snowflake flattening ask: given an events table with a &lt;code&gt;VARIANT&lt;/code&gt; payload containing a nested &lt;code&gt;orders&lt;/code&gt; array, produce one row per (event, order) pair. &lt;code&gt;LATERAL FLATTEN&lt;/code&gt; is the flagship Snowflake pattern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given a Snowflake &lt;code&gt;events(id, payload VARIANT)&lt;/code&gt; table where each payload has an &lt;code&gt;orders&lt;/code&gt; array, write a query using &lt;code&gt;LATERAL FLATTEN&lt;/code&gt; that produces &lt;code&gt;(event_id, user, order_id, total)&lt;/code&gt;. Explain the &lt;code&gt;input =&amp;gt;&lt;/code&gt; argument and the &lt;code&gt;OUTER =&amp;gt; true&lt;/code&gt; option.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;payload&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;{"user": "u1", "orders": [{"id": 1, "total": 10}, {"id": 2, "total": 25}]}&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;td&gt;{"user": "u2", "orders": []}&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- INNER — drops rows where the array is empty or NULL&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;                    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;user&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="k"&gt;LATERAL&lt;/span&gt; &lt;span class="n"&gt;FLATTEN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- OUTER — preserves rows with empty arrays (order_id / total NULL)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;                    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;user&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="k"&gt;LATERAL&lt;/span&gt; &lt;span class="n"&gt;FLATTEN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;outer&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;LATERAL FLATTEN(input =&amp;gt; e.payload:orders)&lt;/code&gt; reads: "for each event, flatten the &lt;code&gt;orders&lt;/code&gt; array into rows." The &lt;code&gt;input =&amp;gt;&lt;/code&gt; argument is the array to flatten.&lt;/li&gt;
&lt;li&gt;Each output row from FLATTEN has columns &lt;code&gt;SEQ&lt;/code&gt;, &lt;code&gt;KEY&lt;/code&gt;, &lt;code&gt;PATH&lt;/code&gt;, &lt;code&gt;INDEX&lt;/code&gt;, &lt;code&gt;VALUE&lt;/code&gt;, &lt;code&gt;THIS&lt;/code&gt;. &lt;code&gt;VALUE&lt;/code&gt; is the array element as &lt;code&gt;VARIANT&lt;/code&gt;; that's what you extract from.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;o.value:id::int&lt;/code&gt; navigates into the element with colon-notation and casts to &lt;code&gt;INT&lt;/code&gt;. Same colon syntax as any Snowflake VARIANT access.&lt;/li&gt;
&lt;li&gt;The default is an inner join — events with empty &lt;code&gt;orders&lt;/code&gt; arrays disappear from the output. To preserve them (like a LEFT JOIN), add &lt;code&gt;outer =&amp;gt; true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LATERAL&lt;/code&gt; before FLATTEN is Snowflake syntax for "correlate this table function with the current outer row." Without &lt;code&gt;LATERAL&lt;/code&gt;, &lt;code&gt;FLATTEN&lt;/code&gt; can't reference &lt;code&gt;e.payload&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output (INNER variant).&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;event_id&lt;/th&gt;
&lt;th&gt;user&lt;/th&gt;
&lt;th&gt;order_id&lt;/th&gt;
&lt;th&gt;total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Output (OUTER variant).&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;event_id&lt;/th&gt;
&lt;th&gt;user&lt;/th&gt;
&lt;th&gt;order_id&lt;/th&gt;
&lt;th&gt;total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; In Snowflake, &lt;code&gt;LATERAL FLATTEN&lt;/code&gt; is the default answer for any VARIANT flattening. Use &lt;code&gt;outer =&amp;gt; true&lt;/code&gt; when the outer row should survive empty arrays; use &lt;code&gt;recursive =&amp;gt; true&lt;/code&gt; when you want to explode nested sub-objects automatically.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — BigQuery UNNEST(JSON_QUERY_ARRAY) with per-element extraction
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; BigQuery's flattening pattern: use &lt;code&gt;JSON_QUERY_ARRAY&lt;/code&gt; to get the array as an &lt;code&gt;ARRAY&amp;lt;STRING&amp;gt;&lt;/code&gt; (or &lt;code&gt;ARRAY&amp;lt;JSON&amp;gt;&lt;/code&gt; for native &lt;code&gt;JSON&lt;/code&gt;), then &lt;code&gt;UNNEST&lt;/code&gt; it in the &lt;code&gt;FROM&lt;/code&gt; clause. Per-element extraction uses &lt;code&gt;JSON_VALUE&lt;/code&gt; on the unnested element.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given a BigQuery &lt;code&gt;events(id STRING, payload JSON)&lt;/code&gt; table where each payload has an &lt;code&gt;orders&lt;/code&gt; array, write a query that produces &lt;code&gt;(event_id, user, order_id, total)&lt;/code&gt;. Show both the native-JSON form and the STRING-of-JSON form.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;payload&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;{"user": "u1", "orders": [{"id": 1, "total": 10}, {"id": 2, "total": 25}]}&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;td&gt;{"user": "u2", "orders": [{"id": 3, "total": 40}]}&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Native JSON type&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;                          &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.user'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.total'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="k"&gt;UNNEST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JSON_QUERY_ARRAY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.orders'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- STRING-of-JSON — same shape, legacy alias&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;                                 &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;JSON_EXTRACT_SCALAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.user'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;JSON_EXTRACT_SCALAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;JSON_EXTRACT_SCALAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.total'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="k"&gt;UNNEST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JSON_EXTRACT_ARRAY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.orders'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;JSON_QUERY_ARRAY(e.payload, '$.orders')&lt;/code&gt; returns an &lt;code&gt;ARRAY&amp;lt;JSON&amp;gt;&lt;/code&gt; (native) or &lt;code&gt;ARRAY&amp;lt;STRING&amp;gt;&lt;/code&gt; (legacy) containing the array elements.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;UNNEST(array)&lt;/code&gt; flattens the array to rows in a &lt;code&gt;FROM&lt;/code&gt; clause. Each row is one element, aliased as &lt;code&gt;o&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The implicit correlation between &lt;code&gt;events e&lt;/code&gt; and &lt;code&gt;UNNEST&lt;/code&gt; is what BigQuery calls a "correlated cross join" — it runs the UNNEST per outer row automatically. No &lt;code&gt;LATERAL&lt;/code&gt; keyword needed.&lt;/li&gt;
&lt;li&gt;Per-element extraction uses &lt;code&gt;JSON_VALUE(o, '$.id')&lt;/code&gt; on the unnested element. Because the element is native JSON, we still need to path into it.&lt;/li&gt;
&lt;li&gt;The STRING-of-JSON path (&lt;code&gt;JSON_EXTRACT_ARRAY&lt;/code&gt; + &lt;code&gt;JSON_EXTRACT_SCALAR&lt;/code&gt;) is the legacy form for &lt;code&gt;STRING&lt;/code&gt;-typed columns. Use the native &lt;code&gt;JSON&lt;/code&gt; path for new work; it has better plan characteristics on BigQuery's columnar engine.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;event_id&lt;/th&gt;
&lt;th&gt;user&lt;/th&gt;
&lt;th&gt;order_id&lt;/th&gt;
&lt;th&gt;total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For BigQuery, always prefer native &lt;code&gt;JSON&lt;/code&gt; columns + &lt;code&gt;JSON_VALUE&lt;/code&gt; / &lt;code&gt;JSON_QUERY_ARRAY&lt;/code&gt; on new work. Only use &lt;code&gt;STRING&lt;/code&gt;-of-JSON + &lt;code&gt;JSON_EXTRACT_SCALAR&lt;/code&gt; when you're stuck with a legacy schema — the migration cost is often worth the query-plan improvement.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — Postgres jsonb_to_recordset for typed flattening
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Postgres's &lt;code&gt;jsonb_to_recordset&lt;/code&gt; is the concise cousin of ANSI &lt;code&gt;JSON_TABLE&lt;/code&gt; — it emits typed rows given a column list, without the &lt;code&gt;PATH&lt;/code&gt; clauses. Great when the array elements are flat objects with a known schema.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given a Postgres &lt;code&gt;events(id int, payload jsonb)&lt;/code&gt; table with a nested &lt;code&gt;orders&lt;/code&gt; array of flat objects, write a query using &lt;code&gt;jsonb_to_recordset&lt;/code&gt; that produces &lt;code&gt;(event_id, user, order_id, total)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;payload&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;{"user": "u1", "orders": [{"id": 1, "total": 10}, {"id": 2, "total": 25}]}&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;td&gt;{"user": "u2", "orders": [{"id": 3, "total": 40}]}&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- jsonb_to_recordset — typed rows, no PATH clauses&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;                      &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'user'&lt;/span&gt;      &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;                      &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="k"&gt;LATERAL&lt;/span&gt; &lt;span class="n"&gt;jsonb_to_recordset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'orders'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;jsonb_to_recordset(jsonb_array)&lt;/code&gt; is a set-returning function that emits one row per array element. Each element must be a flat object whose keys match the requested column names.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;AS o(id int, total numeric)&lt;/code&gt; clause names the columns and their types. Postgres reads the JSON object per element and maps &lt;code&gt;id&lt;/code&gt; → &lt;code&gt;id int&lt;/code&gt;, &lt;code&gt;total&lt;/code&gt; → &lt;code&gt;total numeric&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LATERAL&lt;/code&gt; allows the function to reference &lt;code&gt;e.payload&lt;/code&gt; — the outer row. Without &lt;code&gt;LATERAL&lt;/code&gt;, Postgres can't correlate.&lt;/li&gt;
&lt;li&gt;The outer &lt;code&gt;SELECT&lt;/code&gt; projects both event columns (&lt;code&gt;e.&lt;/code&gt;) and per-order columns (&lt;code&gt;o.&lt;/code&gt;). The result is one row per (event, order) pair.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;jsonb_to_recordset&lt;/code&gt; requires flat objects — nested keys don't work. For nested arrays or nested objects, fall back to &lt;code&gt;jsonb_array_elements&lt;/code&gt; + &lt;code&gt;LATERAL&lt;/code&gt; or PostgreSQL 17's ANSI &lt;code&gt;JSON_TABLE&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;event_id&lt;/th&gt;
&lt;th&gt;user&lt;/th&gt;
&lt;th&gt;order_id&lt;/th&gt;
&lt;th&gt;total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For Postgres &amp;lt; 17, &lt;code&gt;jsonb_to_recordset&lt;/code&gt; is the concise pattern when the array elements are flat. For nested arrays, chain &lt;code&gt;LATERAL jsonb_array_elements&lt;/code&gt;. For Postgres 17+, &lt;code&gt;JSON_TABLE&lt;/code&gt; is the ANSI answer and the future default.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on nested-array flattening
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "You have events with a doubly-nested payload: each session has a list of events, and each event has a list of tags. Write a portable flattener across ANSI dialects that produces one row per (session, event, tag) — and explain why the flat product matters."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using NESTED PATH with JSON_TABLE
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ANSI JSON_TABLE with NESTED PATH — Oracle, MySQL 8, SQL Server 2022, Postgres 17&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;            &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sessions&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="n"&gt;APPLY&lt;/span&gt; &lt;span class="n"&gt;JSON_TABLE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'$.events[*]'&lt;/span&gt;
  &lt;span class="n"&gt;COLUMNS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;event_id&lt;/span&gt;   &lt;span class="nb"&gt;INT&lt;/span&gt;       &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_kind&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.kind'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;NESTED&lt;/span&gt; &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.tags[*]'&lt;/span&gt;
      &lt;span class="n"&gt;COLUMNS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;tags_registry&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- CROSS APPLY == LATERAL in ANSI dialects; syntax varies per engine.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;What runs&lt;/th&gt;
&lt;th&gt;Cardinality&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Outer &lt;code&gt;sessions&lt;/code&gt; scan&lt;/td&gt;
&lt;td&gt;N sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Row-generator path &lt;code&gt;$.events[*]&lt;/code&gt; fires per session&lt;/td&gt;
&lt;td&gt;N × avg(events/session)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;NESTED PATH '$.tags[*]'&lt;/code&gt; fires per event&lt;/td&gt;
&lt;td&gt;N × avg(events) × avg(tags)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Each (event, tag) pair emits a row&lt;/td&gt;
&lt;td&gt;Cartesian per level&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Outer join to &lt;code&gt;tags_registry&lt;/code&gt; filters/joins tags&lt;/td&gt;
&lt;td&gt;Final result set&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The nested-path form emits one row per leaf (session, event, tag) tuple. Every nesting level multiplies the cardinality — this is the "flat product" and is exactly what a relational join wants.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;session_id&lt;/th&gt;
&lt;th&gt;event_id&lt;/th&gt;
&lt;th&gt;event_kind&lt;/th&gt;
&lt;th&gt;tag&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;s1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;click&lt;/td&gt;
&lt;td&gt;premium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;s1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;click&lt;/td&gt;
&lt;td&gt;mobile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;s1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;view&lt;/td&gt;
&lt;td&gt;premium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;s2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;purchase&lt;/td&gt;
&lt;td&gt;premium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;s2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;purchase&lt;/td&gt;
&lt;td&gt;high-value&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;NESTED PATH nests inside COLUMNS&lt;/strong&gt;&lt;/strong&gt; — the outer &lt;code&gt;PATH '$.events[*]'&lt;/code&gt; generates one row per event; the inner &lt;code&gt;NESTED PATH '$.tags[*]'&lt;/code&gt; generates one row per (event, tag) pair. The two combine into the Cartesian product per session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;PATH '$' refers to the current scalar&lt;/strong&gt;&lt;/strong&gt; — inside &lt;code&gt;NESTED PATH&lt;/code&gt;, the column mapping &lt;code&gt;tag VARCHAR(64) PATH '$'&lt;/code&gt; reads: "the current tag element is a scalar; take its value as-is." Useful for arrays of strings, not objects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cartesian expansion is desired&lt;/strong&gt;&lt;/strong&gt; — a row per leaf tuple is what enables relational joins downstream. Every session, every event, every tag becomes one row that can join, filter, aggregate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;NULL handling on empty nested arrays&lt;/strong&gt;&lt;/strong&gt; — the default is &lt;code&gt;NULL ON EMPTY&lt;/code&gt;; sessions with no tags still emit event rows with &lt;code&gt;tag = NULL&lt;/code&gt;. Add &lt;code&gt;ERROR ON EMPTY&lt;/code&gt; if you want strict schemas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — cost is O(sessions × avg(events) × avg(tags)). For big multipliers, this can be a lot of rows — the interviewer expects you to recognise the cardinality risk and consider filtering before flattening.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — data transformation&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Data transformation problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/data-transformation" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — JSON&lt;/span&gt;
&lt;strong&gt;JSON flattening drills&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/json" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  5. Dialect matrix + performance
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The five-engine JSON matrix: &lt;code&gt;bigquery json&lt;/code&gt; vs &lt;code&gt;snowflake variant&lt;/code&gt; vs Postgres jsonb vs SQL Server JSON vs MySQL JSON — storage, index, and pruning tell the whole story
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;every engine has a JSON storage type, an index shape, and a pruning story — Postgres pairs &lt;code&gt;jsonb&lt;/code&gt; with GIN, MySQL pairs &lt;code&gt;JSON&lt;/code&gt; with a functional index on a generated column, SQL Server pairs &lt;code&gt;NVARCHAR(MAX)&lt;/code&gt; with a computed column + index, Snowflake pairs &lt;code&gt;VARIANT&lt;/code&gt; with automatic clustering + micro-partition prune, BigQuery pairs the native &lt;code&gt;JSON&lt;/code&gt; type with partitioning + clustering on a materialised column, and the interviewer wants to see you name the pair for each dialect&lt;/strong&gt;. Once you say the pair out loud, the performance interview surface reduces to a lookup.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F725hv3v6f1zlb79q9vir.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F725hv3v6f1zlb79q9vir.jpeg" alt="Visual diagram of JSON dialect performance matrix — top a 5-dialect storage/index comparison (Postgres jsonb + GIN, MySQL JSON + generated column, SQL Server JSON + computed column, Snowflake VARIANT + auto clustering, BigQuery native JSON + partitioning); bottom a decision card showing when a materialised column beats JSON extraction; on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storage cost across five engines.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Postgres jsonb&lt;/strong&gt; — binary encoded, deduped keys. Typically 5-15% smaller than raw JSON. Fixed per-row overhead of ~4 bytes; body scales with content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres json&lt;/strong&gt; — raw text. Ordered, no dedup. ~10-15% larger than jsonb.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MySQL JSON&lt;/strong&gt; — binary encoded, similar dedup story to jsonb. Slightly larger than jsonb due to different binary format.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL Server&lt;/strong&gt; — no dedicated JSON type; stored as &lt;code&gt;NVARCHAR(MAX)&lt;/code&gt;. Raw text. ~2× larger than jsonb (Unicode) unless compressed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snowflake VARIANT&lt;/strong&gt; — columnar-encoded semi-structured storage. Each JSON path becomes a virtual sub-column; storage is per-path columnar. Typically 20-50% smaller than raw JSON due to columnar compression.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BigQuery native JSON&lt;/strong&gt; — columnar storage similar to Snowflake VARIANT. Per-path pruning at query time. Much smaller than &lt;code&gt;STRING&lt;/code&gt;-of-JSON.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Index strategies.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Postgres jsonb + GIN&lt;/strong&gt; — the canonical pair. &lt;code&gt;jsonb_path_ops&lt;/code&gt; for containment-only; &lt;code&gt;jsonb_ops&lt;/code&gt; for the full operator set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres jsonb + functional expression index&lt;/strong&gt; — for one hot path: &lt;code&gt;CREATE INDEX ON events ((payload-&amp;gt;&amp;gt;'user_id'))&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres jsonb + generated column + BTREE&lt;/strong&gt; — the analytics-engineer's pattern; materialise the extraction, index the column.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MySQL JSON + generated column + BTREE&lt;/strong&gt; — MySQL doesn't support functional indexes on JSON expressions; the workaround is a &lt;code&gt;GENERATED ... VIRTUAL / STORED&lt;/code&gt; column + a normal BTREE.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL Server + computed column + index&lt;/strong&gt; — &lt;code&gt;ALTER TABLE events ADD user_id AS JSON_VALUE(payload, '$.user_id') PERSISTED; CREATE INDEX ix_user_id ON events(user_id);&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snowflake VARIANT + automatic clustering&lt;/strong&gt; — Snowflake clusters micro-partitions on any JSON path automatically; no explicit index required. &lt;code&gt;ALTER TABLE events CLUSTER BY (payload:user_id::string);&lt;/code&gt; for explicit clustering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BigQuery + partitioning + clustering&lt;/strong&gt; — partition by &lt;code&gt;ts&lt;/code&gt;, cluster by a materialised JSON extraction. Native JSON columns support direct clustering on &lt;code&gt;JSON_VALUE(payload, '$.user_id')&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pruning stories per engine.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Postgres&lt;/strong&gt; — GIN doesn't prune whole pages; it produces a bitmap of candidate rows. BTREE indexes on materialised columns prune at the page level.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MySQL&lt;/strong&gt; — BTREE on a generated column prunes pages at BTREE speed. No JSON-native pruning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL Server&lt;/strong&gt; — computed column index prunes pages; JSON functions themselves don't prune.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snowflake VARIANT&lt;/strong&gt; — micro-partition prune on any clustered JSON path. If a query filters on &lt;code&gt;payload:user_id::string = 'u1'&lt;/code&gt; and the table is clustered on that expression, Snowflake reads only the micro-partitions containing &lt;code&gt;u1&lt;/code&gt;. Massive win for large tables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BigQuery native JSON&lt;/strong&gt; — partition prune on &lt;code&gt;ts&lt;/code&gt;; cluster prune on any clustered materialised JSON extraction. Columnar storage means only the relevant JSON path is read — often 90% cheaper than &lt;code&gt;STRING&lt;/code&gt;-of-JSON.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When a materialised column beats JSON extraction.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hot filter path.&lt;/strong&gt; &lt;code&gt;WHERE JSON_VALUE(payload, '$.status') = 'active'&lt;/code&gt; — every planner struggles with this. Materialise &lt;code&gt;status&lt;/code&gt; as a column, index the column, done.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Join key.&lt;/strong&gt; &lt;code&gt;JOIN users ON JSON_VALUE(payload, '$.user_id') = users.id&lt;/code&gt; — never faster than a plain column join. Materialise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aggregate key.&lt;/strong&gt; &lt;code&gt;GROUP BY JSON_VALUE(payload, '$.plan')&lt;/code&gt; — materialise for planner-friendly hash grouping.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sort key.&lt;/strong&gt; &lt;code&gt;ORDER BY JSON_VALUE(payload, '$.created_at')&lt;/code&gt; — materialise + index.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anything hit by dashboards.&lt;/strong&gt; BI tools generate queries that assume plain columns; materialise every column a dashboard filters on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When to keep the field inside JSON.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rare projections.&lt;/strong&gt; A field only surfaced in one report per month; the JSON extraction overhead is negligible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema drift.&lt;/strong&gt; Fields that change across event types (event-specific payloads); materialising each variant creates schema explosion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deep sub-trees.&lt;/strong&gt; Objects that are always consumed as sub-trees (never as scalars) — extract the sub-tree at query time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Snowflake VARIANT + AUTOMATIC_CLUSTERING deep dive.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Snowflake's storage engine indexes JSON paths automatically as it writes. Each micro-partition tracks min/max stats per JSON path.&lt;/li&gt;
&lt;li&gt;Query planner uses these stats to skip micro-partitions that can't match the predicate.&lt;/li&gt;
&lt;li&gt;For a query like &lt;code&gt;WHERE payload:user_id::string = 'u1'&lt;/code&gt;, Snowflake reads only the micro-partitions where &lt;code&gt;user_id&lt;/code&gt; includes &lt;code&gt;u1&lt;/code&gt; in its min/max range.&lt;/li&gt;
&lt;li&gt;Explicit &lt;code&gt;CLUSTER BY (payload:user_id::string)&lt;/code&gt; forces the physical layout to concentrate matching rows in fewer micro-partitions — accelerates automatic clustering.&lt;/li&gt;
&lt;li&gt;Great for wide tables with many JSON paths and workloads that filter on many different paths across queries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;BigQuery native JSON vs STRING-of-JSON.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Native &lt;code&gt;JSON&lt;/code&gt; type — columnar per-path storage. Only the queried paths are read from disk. Pruning is native.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;STRING&lt;/code&gt;-of-JSON — the whole column is read as text, then parsed on the fly per row. Massively more expensive.&lt;/li&gt;
&lt;li&gt;BigQuery bills by bytes read; native &lt;code&gt;JSON&lt;/code&gt; bills only for the paths you extract. A &lt;code&gt;WHERE JSON_VALUE(payload, '$.status')&lt;/code&gt; query on a 10TB table might read 100GB (native) vs 10TB (STRING). 100× cost difference.&lt;/li&gt;
&lt;li&gt;Migration path: &lt;code&gt;CREATE TABLE events_native AS SELECT id, ts, PARSE_JSON(payload) AS payload FROM events_string; DROP TABLE events_string;&lt;/code&gt; (with appropriate care for pipelines).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common &lt;code&gt;bigquery json&lt;/code&gt; performance probes.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"How would you migrate a 10TB &lt;code&gt;STRING&lt;/code&gt;-of-JSON table to native JSON?" — &lt;code&gt;PARSE_JSON&lt;/code&gt; in a &lt;code&gt;CREATE TABLE AS SELECT&lt;/code&gt;, verify with a dry-run cost estimate, cut over with a view rename.&lt;/li&gt;
&lt;li&gt;"What's the cost difference between &lt;code&gt;JSON_VALUE&lt;/code&gt; on native vs STRING?" — often 10-100× because native only reads the queried path.&lt;/li&gt;
&lt;li&gt;"How do you cluster a BigQuery JSON table?" — cluster on a materialised &lt;code&gt;JSON_VALUE(payload, '$.hot_key')&lt;/code&gt; column; the JSON column itself is not clusterable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common &lt;code&gt;snowflake variant&lt;/code&gt; performance probes.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"What is &lt;code&gt;AUTOMATIC_CLUSTERING&lt;/code&gt;?" — Snowflake's background service that reorganises micro-partitions to keep them well-clustered; runs continuously.&lt;/li&gt;
&lt;li&gt;"How does micro-partition prune work on a JSON path?" — Snowflake tracks min/max per path per micro-partition; the planner skips partitions whose stats can't match the predicate.&lt;/li&gt;
&lt;li&gt;"When would you &lt;code&gt;ALTER TABLE CLUSTER BY&lt;/code&gt; a VARIANT column?" — when queries filter heavily on one path and automatic clustering isn't converging fast enough.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — Postgres jsonb vs generated column + BTREE benchmarking
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The most common tuning debate: "should I index the JSON column with GIN, or materialise the hot path as a generated column with a BTREE?" The answer depends on query patterns. Show the interviewer a concrete comparison.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given a Postgres &lt;code&gt;events(id, ts, payload jsonb)&lt;/code&gt; table with 100M rows, compare the plan and speed of three options for &lt;code&gt;WHERE payload-&amp;gt;&amp;gt;'user_id' = 'u1'&lt;/code&gt;: (a) no index, (b) GIN on payload, (c) generated column + BTREE.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;ts&lt;/th&gt;
&lt;th&gt;payload&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2026-07-01&lt;/td&gt;
&lt;td&gt;{"user_id": "u1", "kind": "click"}&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Option A — no index&lt;/span&gt;
&lt;span class="c1"&gt;-- Plan: Seq Scan on events (cost=0.00..3000000.00 rows=100k)&lt;/span&gt;

&lt;span class="c1"&gt;-- Option B — GIN on payload (jsonb_ops for @&amp;gt;)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;ix_events_gin&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;GIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="n"&gt;jsonb_path_ops&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Query rewrite for GIN to be used&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;@&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'{"user_id": "u1"}'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Plan: Bitmap Heap Scan on events (cost=100..500 rows=100k)&lt;/span&gt;

&lt;span class="c1"&gt;-- Option C — generated column + BTREE&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
  &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;GENERATED&lt;/span&gt; &lt;span class="n"&gt;ALWAYS&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'user_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;STORED&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;ix_events_user_id&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'u1'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Plan: Index Scan using ix_events_user_id (cost=0.42..12.44 rows=100k)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Option A (no index): a full sequential scan on 100M rows. Runtime scales linearly with table size — unacceptable for interactive queries.&lt;/li&gt;
&lt;li&gt;Option B (GIN + rewrite to &lt;code&gt;@&amp;gt;&lt;/code&gt;): the GIN indexes containment fragments; the query planner picks a bitmap heap scan. Fast, but requires rewriting the query from &lt;code&gt;-&amp;gt;&amp;gt;&lt;/code&gt; to &lt;code&gt;@&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Option C (generated column + BTREE): the plain BTREE is the fastest option for equality. The generated column stays in sync automatically. Query is unchanged from the "plain column" mental model.&lt;/li&gt;
&lt;li&gt;Trade-offs: GIN supports many query patterns (containment, existence) with one index. Generated column + BTREE supports one pattern per column but is faster and smaller.&lt;/li&gt;
&lt;li&gt;Rule of thumb: for 1-2 hot keys, generated column + BTREE wins. For many query patterns or deep containment, GIN wins.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output (plan-and-timing comparison).&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Runtime (100M rows, sel=0.1%)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No index&lt;/td&gt;
&lt;td&gt;Seq Scan&lt;/td&gt;
&lt;td&gt;30-60s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GIN + &lt;code&gt;@&amp;gt;&lt;/code&gt; rewrite&lt;/td&gt;
&lt;td&gt;Bitmap Heap Scan&lt;/td&gt;
&lt;td&gt;50-200ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generated column + BTREE&lt;/td&gt;
&lt;td&gt;Index Scan&lt;/td&gt;
&lt;td&gt;5-20ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; When the workload is dominated by 1-2 hot extractions with plain equality, always materialise as generated columns with BTREEs. Fall back to GIN when you need containment (&lt;code&gt;@&amp;gt;&lt;/code&gt;) or existence (&lt;code&gt;?&lt;/code&gt;) semantics.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — Snowflake VARIANT clustering for a hot user_id filter
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Snowflake's flagship performance pattern: cluster a &lt;code&gt;VARIANT&lt;/code&gt; column on a hot JSON path. Micro-partition prune reads only the relevant partitions, cutting query cost dramatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given a Snowflake &lt;code&gt;events(id, ts, payload VARIANT)&lt;/code&gt; table with 10TB and a hot query pattern &lt;code&gt;WHERE payload:user_id::string = 'u1'&lt;/code&gt;, write the DDL to cluster on the user_id path and estimate the pruning gain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;ts&lt;/th&gt;
&lt;th&gt;payload&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2026-07-01&lt;/td&gt;
&lt;td&gt;{"user_id": "u1", "kind": "click"}&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Cluster the table on the hot JSON path&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;CLUSTER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Run the query&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'u1'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'7 days'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Snowflake will automatically re-cluster in the background;&lt;/span&gt;
&lt;span class="c1"&gt;-- explicit CLUSTER BY declaration accelerates convergence.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;ALTER TABLE events CLUSTER BY (payload:user_id::string)&lt;/code&gt; declares the clustering key. Snowflake's &lt;code&gt;AUTOMATIC_CLUSTERING&lt;/code&gt; service will start reorganising micro-partitions in the background.&lt;/li&gt;
&lt;li&gt;Once well-clustered, matching &lt;code&gt;user_id&lt;/code&gt; values are concentrated in a small subset of micro-partitions. Snowflake's per-partition min/max stats let the planner skip the rest.&lt;/li&gt;
&lt;li&gt;For a table with 10TB in 100k micro-partitions and a hot user with 0.01% of rows, well-clustered reads scan ~10-100 micro-partitions (1-10GB), not the full 10TB.&lt;/li&gt;
&lt;li&gt;The query also filters on &lt;code&gt;ts &amp;gt; NOW() - 7 days&lt;/code&gt;. Snowflake tables are implicitly partitioned by insert order, so time-based filters also prune. Combining both cuts cost by 5-6 orders of magnitude.&lt;/li&gt;
&lt;li&gt;Cost model: Snowflake bills by bytes scanned. A well-clustered query on this pattern is often &amp;lt;$0.01; the same query on an unclustered table is $10-100.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;ts&lt;/th&gt;
&lt;th&gt;kind&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2026-07-01&lt;/td&gt;
&lt;td&gt;click&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For any Snowflake VARIANT table with a hot JSON-path filter, declare &lt;code&gt;CLUSTER BY (payload:path::type)&lt;/code&gt;. Automatic clustering handles the rest. The pruning gain is often 100-10000× on large tables.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — BigQuery native JSON migration cost analysis
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Migrating a 10TB &lt;code&gt;STRING&lt;/code&gt;-of-JSON table to native &lt;code&gt;JSON&lt;/code&gt; is often a 10-100× cost win. The math is simple: BigQuery only reads the queried JSON paths on native columns; on &lt;code&gt;STRING&lt;/code&gt;-of-JSON, it reads the whole column and parses on the fly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given a BigQuery &lt;code&gt;events_string(id, ts, payload STRING)&lt;/code&gt; table with 10TB and daily queries that extract 3 out of 20 payload keys, estimate the cost saving of migrating to a native &lt;code&gt;JSON&lt;/code&gt; column and show the migration DDL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;ts&lt;/th&gt;
&lt;th&gt;payload (STRING)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2026-07-01&lt;/td&gt;
&lt;td&gt;{"user_id":"u1","kind":"click","other_18_keys":"..."}&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Step 1 — create the native JSON table via CTAS&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="nv"&gt;`project.dataset.events_native`&lt;/span&gt;
&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;CLUSTER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id_mat&lt;/span&gt;  &lt;span class="c1"&gt;-- optional materialised column&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;PARSE_JSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.user_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;user_id_mat&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;`project.dataset.events_string`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 2 — verify with a dry-run query&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;`project.dataset.events_native`&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.user_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'u1'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Dry run reports bytes processed — expect ~5% of the original.&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 3 — swap via view&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="nv"&gt;`project.dataset.events`&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;`project.dataset.events_native`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;PARSE_JSON(payload)&lt;/code&gt; converts the &lt;code&gt;STRING&lt;/code&gt; column into native &lt;code&gt;JSON&lt;/code&gt;. BigQuery stores the parsed representation columnar — one virtual column per JSON path.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;CTAS&lt;/code&gt; also creates a materialised &lt;code&gt;user_id_mat&lt;/code&gt; column that's cluster-eligible. Native JSON columns can't be clustered directly; the workaround is a materialised extraction.&lt;/li&gt;
&lt;li&gt;Cost model: &lt;code&gt;STRING&lt;/code&gt;-of-JSON queries scan the entire &lt;code&gt;payload&lt;/code&gt; column (say 8TB out of 10TB). Native &lt;code&gt;JSON&lt;/code&gt; queries scan only the queried paths (say 400GB — 5% of total). 20× cost saving.&lt;/li&gt;
&lt;li&gt;Migration cost: one-time CTAS scans 10TB (~$50 at $5/TB), the ongoing query cost drops 20×.&lt;/li&gt;
&lt;li&gt;Downstream compatibility: existing queries that use &lt;code&gt;JSON_EXTRACT_SCALAR&lt;/code&gt; on a &lt;code&gt;STRING&lt;/code&gt; column need to be rewritten to &lt;code&gt;JSON_VALUE&lt;/code&gt; on the native column. Use a view during the transition to minimise breakage.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output (cost estimate).&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query pattern&lt;/th&gt;
&lt;th&gt;STRING-of-JSON cost&lt;/th&gt;
&lt;th&gt;Native JSON cost&lt;/th&gt;
&lt;th&gt;Saving&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Filter on 1 key, project 3 keys&lt;/td&gt;
&lt;td&gt;10TB scan&lt;/td&gt;
&lt;td&gt;500GB scan&lt;/td&gt;
&lt;td&gt;95%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filter on 5 keys&lt;/td&gt;
&lt;td&gt;10TB scan&lt;/td&gt;
&lt;td&gt;2TB scan&lt;/td&gt;
&lt;td&gt;80%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aggregate over the full payload&lt;/td&gt;
&lt;td&gt;10TB scan&lt;/td&gt;
&lt;td&gt;10TB scan&lt;/td&gt;
&lt;td&gt;~0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For any BigQuery table above 100GB where JSON queries touch a small subset of keys, migrate to the native &lt;code&gt;JSON&lt;/code&gt; type. The cost saving pays for the migration in weeks or days, and the query plans improve dramatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on choosing the right JSON stack
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "You're greenfielding a data platform that will ingest 100M events/day, each with a 2KB JSON payload. You need to serve ad-hoc analytics with sub-10s latency. Which warehouse do you pick, which storage type, which index / cluster strategy, and why?"&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a 5-question warehouse-picking framework
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Warehouse + JSON storage picker — 5 questions

1. Read pattern — ad-hoc analytics or point reads?
   ad-hoc     → columnar warehouse (Snowflake / BigQuery / Redshift)
   point read → OLTP (Postgres + jsonb)

2. Volume — is it above 10TB in the JSON column?
   yes → Snowflake VARIANT or BigQuery native JSON (columnar per-path)
   no  → Postgres jsonb (GIN + generated cols is enough)

3. Filter selectivity — do most queries filter on 1-2 hot keys?
   yes → cluster / partition on materialised extractions
   no  → let automatic clustering handle it (Snowflake) or add many gen cols

4. Schema drift — how fast do new keys arrive?
   fast (weekly) → VARIANT / native JSON — schema-on-read
   slow (monthly) → promote hot keys to plain columns

5. Portability — one warehouse or multi-cloud?
   one warehouse → pick the native stack (VARIANT / native JSON / jsonb)
   multi         → dbt macro dispatch, minimise dialect-specific SQL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;Q1&lt;/th&gt;
&lt;th&gt;Q2&lt;/th&gt;
&lt;th&gt;Q3&lt;/th&gt;
&lt;th&gt;Q4&lt;/th&gt;
&lt;th&gt;Q5&lt;/th&gt;
&lt;th&gt;Pick&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;100M events/day, ad-hoc, sub-10s&lt;/td&gt;
&lt;td&gt;ad-hoc&lt;/td&gt;
&lt;td&gt;20TB/mo&lt;/td&gt;
&lt;td&gt;2 hot&lt;/td&gt;
&lt;td&gt;fast&lt;/td&gt;
&lt;td&gt;one&lt;/td&gt;
&lt;td&gt;Snowflake VARIANT + cluster on user_id&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100M events/day, OLTP + BI&lt;/td&gt;
&lt;td&gt;mixed&lt;/td&gt;
&lt;td&gt;20TB/mo&lt;/td&gt;
&lt;td&gt;5 hot&lt;/td&gt;
&lt;td&gt;fast&lt;/td&gt;
&lt;td&gt;one&lt;/td&gt;
&lt;td&gt;BigQuery native JSON + clustering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1M rows/day, transactional&lt;/td&gt;
&lt;td&gt;point&lt;/td&gt;
&lt;td&gt;1GB&lt;/td&gt;
&lt;td&gt;1 hot&lt;/td&gt;
&lt;td&gt;slow&lt;/td&gt;
&lt;td&gt;one&lt;/td&gt;
&lt;td&gt;Postgres jsonb + generated col + BTREE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-cloud, portable analytics&lt;/td&gt;
&lt;td&gt;ad-hoc&lt;/td&gt;
&lt;td&gt;5TB/mo&lt;/td&gt;
&lt;td&gt;3 hot&lt;/td&gt;
&lt;td&gt;fast&lt;/td&gt;
&lt;td&gt;multi&lt;/td&gt;
&lt;td&gt;Postgres jsonb (self-hosted) + dbt macros&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The framework turns "which warehouse and how do I store JSON?" from a taste question into a mechanical decision. The answers pop out from the volume + read pattern + schema drift axes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pick&lt;/th&gt;
&lt;th&gt;Storage&lt;/th&gt;
&lt;th&gt;Index / cluster&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Snowflake VARIANT&lt;/td&gt;
&lt;td&gt;VARIANT columnar&lt;/td&gt;
&lt;td&gt;cluster on hot path&lt;/td&gt;
&lt;td&gt;best columnar + auto-clustering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BigQuery native JSON&lt;/td&gt;
&lt;td&gt;native JSON&lt;/td&gt;
&lt;td&gt;partition + cluster on materialised col&lt;/td&gt;
&lt;td&gt;best cost model for path-heavy queries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Postgres jsonb&lt;/td&gt;
&lt;td&gt;jsonb binary&lt;/td&gt;
&lt;td&gt;GIN + generated col + BTREE&lt;/td&gt;
&lt;td&gt;best OLTP + operational simplicity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL Server JSON&lt;/td&gt;
&lt;td&gt;NVARCHAR(MAX)&lt;/td&gt;
&lt;td&gt;computed col + BTREE&lt;/td&gt;
&lt;td&gt;best on Windows/enterprise stacks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL JSON&lt;/td&gt;
&lt;td&gt;JSON binary&lt;/td&gt;
&lt;td&gt;generated col + BTREE&lt;/td&gt;
&lt;td&gt;best on MySQL-only shops&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Read pattern drives the warehouse choice&lt;/strong&gt;&lt;/strong&gt; — point reads are OLTP (Postgres); ad-hoc analytics are OLAP (Snowflake/BigQuery). Everything else follows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Volume drives the storage type&lt;/strong&gt;&lt;/strong&gt; — above 10TB, columnar per-path storage wins on cost by orders of magnitude. Below 10TB, &lt;code&gt;jsonb&lt;/code&gt; is fine and often simpler.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Hot keys drive the index strategy&lt;/strong&gt;&lt;/strong&gt; — one hot key wants a plain generated column + BTREE; five wants clustering; many wants a GIN.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Schema drift decides what to materialise&lt;/strong&gt;&lt;/strong&gt; — fast-drifting fields stay in the JSON payload forever; slow-drifting fields eventually get promoted to plain columns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — every path has a query-plan cost and a storage cost that scale differently. The framework picks the stack where the cost curve matches the workload — the definition of a good design decision.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — SQL&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL interview problem library&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;


&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — JSON&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;JSON storage and indexing drills&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/json" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;





&lt;h2&gt;
  
  
  Cheat sheet — SQL JSON recipe list
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Path extraction across five dialects.&lt;/strong&gt; Postgres: &lt;code&gt;payload-&amp;gt;&amp;gt;'key'&lt;/code&gt; or &lt;code&gt;jsonb_path_query_first(payload, '$.key')&lt;/code&gt;. MySQL 8: &lt;code&gt;payload-&amp;gt;&amp;gt;'$.key'&lt;/code&gt; or &lt;code&gt;JSON_VALUE(payload, '$.key' RETURNING VARCHAR(64))&lt;/code&gt;. SQL Server: &lt;code&gt;JSON_VALUE(payload, '$.key')&lt;/code&gt;. Snowflake: &lt;code&gt;payload:key::string&lt;/code&gt;. BigQuery: &lt;code&gt;JSON_VALUE(payload, '$.key')&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalar vs sub-tree split.&lt;/strong&gt; &lt;code&gt;JSON_VALUE&lt;/code&gt; / &lt;code&gt;-&amp;gt;&amp;gt;&lt;/code&gt; returns a scalar SQL type; &lt;code&gt;JSON_QUERY&lt;/code&gt; / &lt;code&gt;-&amp;gt;&lt;/code&gt; returns a JSON sub-tree. Never mix them — the interviewer will catch it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSONPath primitives.&lt;/strong&gt; &lt;code&gt;$&lt;/code&gt; (root), &lt;code&gt;.key&lt;/code&gt; (child), &lt;code&gt;[i]&lt;/code&gt; (index), &lt;code&gt;[*]&lt;/code&gt; (wildcard), &lt;code&gt;..key&lt;/code&gt; (recursive descent — not SQL Server), &lt;code&gt;[?(@.total &amp;gt; 100)]&lt;/code&gt; (filter — Postgres 12+ and MySQL 8).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres jsonb GIN index recipe.&lt;/strong&gt; &lt;code&gt;CREATE INDEX ix ON t USING GIN (payload jsonb_path_ops);&lt;/code&gt; for containment-only (smaller, faster). Use &lt;code&gt;jsonb_ops&lt;/code&gt; when you also need &lt;code&gt;?&lt;/code&gt; existence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres containment operator.&lt;/strong&gt; &lt;code&gt;WHERE payload @&amp;gt; '{"kind":"purchase"}'&lt;/code&gt; — GIN-indexable, the fastest JSON filter in Postgres.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres functional expression index.&lt;/strong&gt; &lt;code&gt;CREATE INDEX ON t ((payload-&amp;gt;&amp;gt;'user_id'));&lt;/code&gt; for one hot key. Query must use the exact same expression.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres generated column + BTREE.&lt;/strong&gt; &lt;code&gt;ADD COLUMN user_id text GENERATED ALWAYS AS (payload-&amp;gt;&amp;gt;'user_id') STORED;&lt;/code&gt; + &lt;code&gt;CREATE INDEX ON t (user_id);&lt;/code&gt;. Best for hot join / group-by keys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON_TABLE flatten recipe.&lt;/strong&gt; &lt;code&gt;SELECT ... FROM t CROSS APPLY JSON_TABLE(payload, '$.orders[*]' COLUMNS (id INT PATH '$.id', total DECIMAL PATH '$.total')) AS o;&lt;/code&gt;. Oracle, MySQL 8, SQL Server 2022+, Postgres 17+.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snowflake VARIANT + FLATTEN recipe.&lt;/strong&gt; &lt;code&gt;SELECT o.value:id::int, o.value:total::number FROM t, LATERAL FLATTEN(input =&amp;gt; t.payload:orders) o;&lt;/code&gt;. Add &lt;code&gt;OUTER =&amp;gt; true&lt;/code&gt; for LEFT-JOIN semantics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BigQuery UNNEST(JSON_QUERY_ARRAY) recipe.&lt;/strong&gt; &lt;code&gt;SELECT JSON_VALUE(o, '$.id'), JSON_VALUE(o, '$.total') FROM t, UNNEST(JSON_QUERY_ARRAY(payload, '$.orders')) AS o;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres jsonb_to_recordset recipe.&lt;/strong&gt; &lt;code&gt;SELECT o.id, o.total FROM t, LATERAL jsonb_to_recordset(t.payload -&amp;gt; 'orders') AS o(id int, total numeric);&lt;/code&gt;. Flat objects only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NESTED PATH.&lt;/strong&gt; ANSI JSON_TABLE sub-block that flattens a second-level array: &lt;code&gt;NESTED PATH '$.tags[*]' COLUMNS (tag VARCHAR PATH '$')&lt;/code&gt;. Emits one row per (outer, inner) pair.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Null-safe extraction pattern.&lt;/strong&gt; &lt;code&gt;COALESCE(payload-&amp;gt;&amp;gt;'key', 'default')&lt;/code&gt; in Postgres; &lt;code&gt;IFNULL(JSON_VALUE(payload, '$.key'), 'default')&lt;/code&gt; in MySQL / Snowflake; &lt;code&gt;ISNULL(JSON_VALUE(payload, '$.key'), 'default')&lt;/code&gt; in SQL Server; &lt;code&gt;IFNULL(JSON_VALUE(payload, '$.key'), 'default')&lt;/code&gt; in BigQuery.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Materialised column indexing recipe.&lt;/strong&gt; Any hot JSON key that participates in filter, join, group-by, or sort should be materialised as a generated / computed column and indexed with a BTREE. Universal across dialects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snowflake clustering recipe.&lt;/strong&gt; &lt;code&gt;ALTER TABLE t CLUSTER BY (payload:hot_key::string);&lt;/code&gt;. Automatic clustering handles the rest; monitor with &lt;code&gt;SYSTEM$CLUSTERING_INFORMATION&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BigQuery native JSON migration recipe.&lt;/strong&gt; &lt;code&gt;CREATE TABLE new AS SELECT id, ts, PARSE_JSON(payload) AS payload FROM old;&lt;/code&gt;. Then swap via view for zero-downtime cutover.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Containment vs equality.&lt;/strong&gt; &lt;code&gt;@&amp;gt;&lt;/code&gt; (Postgres) tests JSON sub-object containment; &lt;code&gt;= '{"key":"val"}'&lt;/code&gt; tests full-object equality including key order (broken by &lt;code&gt;jsonb&lt;/code&gt; dedup). Use &lt;code&gt;@&amp;gt;&lt;/code&gt; for filtering, never &lt;code&gt;=&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When to use &lt;code&gt;json&lt;/code&gt; over &lt;code&gt;jsonb&lt;/code&gt; in Postgres.&lt;/strong&gt; Almost never — only when key order or whitespace preservation matters (RFC 7396 patches, signature contexts).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;dbt cross-dialect dispatch.&lt;/strong&gt; Write a &lt;code&gt;json_scalar(col, key)&lt;/code&gt; macro that dispatches on &lt;code&gt;target.type&lt;/code&gt; to emit Postgres &lt;code&gt;-&amp;gt;&amp;gt;&lt;/code&gt; vs Snowflake &lt;code&gt;::string&lt;/code&gt; vs BigQuery &lt;code&gt;JSON_VALUE&lt;/code&gt;. One source, dialect-native compiled SQL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MySQL JSON_UNQUOTE gotcha.&lt;/strong&gt; &lt;code&gt;JSON_EXTRACT&lt;/code&gt; returns quoted strings; always wrap with &lt;code&gt;JSON_UNQUOTE&lt;/code&gt; or use &lt;code&gt;-&amp;gt;&amp;gt;&lt;/code&gt; when the value feeds a &lt;code&gt;WHERE&lt;/code&gt;, &lt;code&gt;GROUP BY&lt;/code&gt;, or &lt;code&gt;JOIN&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is the difference between &lt;code&gt;JSON_VALUE&lt;/code&gt; and &lt;code&gt;JSON_EXTRACT&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;JSON_VALUE&lt;/code&gt; returns a scalar SQL value (usually &lt;code&gt;VARCHAR&lt;/code&gt; or a typed value via &lt;code&gt;RETURNING&lt;/code&gt;) — designed for equality, aggregation, and typed downstream use; it errors or returns NULL when the target is a JSON object or array. &lt;code&gt;JSON_EXTRACT&lt;/code&gt; is dialect-specific — MySQL and SQLite ship it, and it returns a JSON value that may be a scalar, an object, or an array with quotes preserved on strings. In MySQL 8, &lt;code&gt;JSON_EXTRACT(payload, '$.name')&lt;/code&gt; returns &lt;code&gt;"Ada"&lt;/code&gt; (with quotes) but &lt;code&gt;JSON_VALUE(payload, '$.name')&lt;/code&gt; returns &lt;code&gt;Ada&lt;/code&gt; (unquoted). The interviewer expects you to name the split — scalar vs sub-tree, quoted vs unquoted — in the first sentence. In SQL Server, BigQuery, and Oracle, the standard split is &lt;code&gt;JSON_VALUE&lt;/code&gt; (scalar) vs &lt;code&gt;JSON_QUERY&lt;/code&gt; (sub-tree); in Postgres, the arrow operators &lt;code&gt;-&amp;gt;&amp;gt;&lt;/code&gt; (scalar text) vs &lt;code&gt;-&amp;gt;&lt;/code&gt; (JSON child) do the same job.&lt;/p&gt;

&lt;h3&gt;
  
  
  When should I use &lt;code&gt;jsonb&lt;/code&gt; over &lt;code&gt;json&lt;/code&gt; in Postgres?
&lt;/h3&gt;

&lt;p&gt;Almost always use &lt;code&gt;jsonb&lt;/code&gt;. The &lt;code&gt;json&lt;/code&gt; type stores raw text — preserving whitespace, key order, and duplicate keys — but is not indexable and requires re-parsing on every read. The &lt;code&gt;jsonb&lt;/code&gt; type stores a binary parsed representation — deduped keys, sorted internally, indexable with GIN — and is ~5-15% smaller. Write cost is ~20-40% higher for &lt;code&gt;jsonb&lt;/code&gt; (parse on insert), but read cost is dramatically lower because navigation is O(1) per key rather than O(n) text-parse. Reach for &lt;code&gt;json&lt;/code&gt; only in narrow cases: preserving RFC 7396 patch documents, canonical signing forms, or when the JSON is never queried and only passed through. For every OLTP or analytics workload where the JSON is read more than once, &lt;code&gt;jsonb&lt;/code&gt; is the correct default.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I flatten a JSON array into rows across dialects?
&lt;/h3&gt;

&lt;p&gt;Each engine ships a different keyword for the same operation. ANSI SQL/JSON &lt;code&gt;JSON_TABLE&lt;/code&gt; — supported by Oracle 12c+, MySQL 8.0.4+, SQL Server 2022+, and Postgres 17+ — is the standard: &lt;code&gt;JSON_TABLE(payload, '$.orders[*]' COLUMNS (id INT PATH '$.id', total DECIMAL PATH '$.total'))&lt;/code&gt;. Snowflake uses &lt;code&gt;LATERAL FLATTEN(input =&amp;gt; payload:orders)&lt;/code&gt; and per-element extraction via &lt;code&gt;value:field::type&lt;/code&gt;. BigQuery pairs &lt;code&gt;UNNEST(JSON_QUERY_ARRAY(payload, '$.orders'))&lt;/code&gt; with per-element &lt;code&gt;JSON_VALUE&lt;/code&gt;. Older Postgres (&amp;lt; 17) uses &lt;code&gt;jsonb_array_elements&lt;/code&gt; or &lt;code&gt;jsonb_to_recordset&lt;/code&gt; in a &lt;code&gt;LATERAL&lt;/code&gt; join. For doubly-nested arrays, ANSI &lt;code&gt;JSON_TABLE&lt;/code&gt; supports a &lt;code&gt;NESTED PATH '$.sub[*]' COLUMNS (...)&lt;/code&gt; sub-block; Snowflake chains &lt;code&gt;LATERAL FLATTEN&lt;/code&gt;; BigQuery chains &lt;code&gt;UNNEST&lt;/code&gt;; Postgres chains &lt;code&gt;LATERAL jsonb_array_elements&lt;/code&gt;. The output shape is always the same — one row per leaf-level tuple — because that's what a relational query wants.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does BigQuery support the native JSON type or do I use STRING?
&lt;/h3&gt;

&lt;p&gt;BigQuery ships a native &lt;code&gt;JSON&lt;/code&gt; type since 2022, and it's the strongly preferred storage for JSON payloads on new tables. Native &lt;code&gt;JSON&lt;/code&gt; stores data columnar per JSON path — meaning a query that extracts one key only reads the bytes for that path, not the whole payload. On a 10TB &lt;code&gt;bigquery json&lt;/code&gt; table, a hot-key filter query typically bills 100-500GB scanned (native) versus 10TB scanned (STRING-of-JSON) — a 20-100× cost improvement plus a proportional speed improvement. The migration path is a &lt;code&gt;CREATE TABLE ... AS SELECT id, ts, PARSE_JSON(payload) AS payload FROM old_table;&lt;/code&gt;, followed by a view rename for zero-downtime cutover. Legacy &lt;code&gt;STRING&lt;/code&gt;-of-JSON columns with &lt;code&gt;JSON_EXTRACT_SCALAR&lt;/code&gt; still work, but every new table and every migration budget should target the native type.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is Snowflake VARIANT and how does it index JSON paths?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;snowflake variant&lt;/code&gt; is Snowflake's self-describing semi-structured type — a single column that holds JSON, Avro, or Parquet-like nested data. Storage is columnar per JSON path (a "virtual sub-column" per path), which means selective extraction only reads the queried paths from disk, not the whole payload. Snowflake's &lt;code&gt;AUTOMATIC_CLUSTERING&lt;/code&gt; service tracks min/max stats per JSON path per micro-partition; when a query filters on &lt;code&gt;payload:user_id::string = 'u1'&lt;/code&gt;, the planner skips micro-partitions whose stats can't match. For hot filter paths, declare an explicit &lt;code&gt;ALTER TABLE t CLUSTER BY (payload:hot_key::string)&lt;/code&gt; to accelerate physical layout convergence — automatic clustering will do it eventually, but explicit declarations converge faster. The typical result on a 10TB table with a well-clustered VARIANT column: a hot-user filter reads 1-10GB instead of 10TB, a 3-4 order-of-magnitude speedup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is JSON in SQL fast enough to replace a document store?
&lt;/h3&gt;

&lt;p&gt;For read-heavy analytics workloads with warehouse-scale volumes, the modern answer is yes — Snowflake VARIANT, BigQuery native JSON, and Postgres &lt;code&gt;jsonb&lt;/code&gt; all outperform document stores on ad-hoc analytical queries, largely because columnar storage and GIN-indexed containment beat MongoDB's default point-read profile at scale. Where document stores still win: sub-10ms OLTP point reads at high QPS (DynamoDB, MongoDB), heavy in-place sub-document updates (MongoDB's atomic operators), and write throughput above 100k QPS. The right question isn't "warehouse vs document store" but "what's the read/write ratio and latency budget?" For 90% of data-engineering workloads — event log analytics, CDC pipelines, feature stores, config tables — JSON-in-SQL is the correct default because it collocates the semi-structured data with the relational world, reducing your operational surface to one system, one backup, one auth model. Document stores earn their keep in the remaining 10% of point-read-heavy OLTP.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practice on PipeCode
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Drill the &lt;a href="https://pipecode.ai/explore/practice/topic/json" rel="noopener noreferrer"&gt;JSON practice library →&lt;/a&gt; for path extraction, containment, and JSON_TABLE flattening reps across all five dialects.&lt;/li&gt;
&lt;li&gt;Rehearse on &lt;a href="https://pipecode.ai/explore/practice/topic/parsing" rel="noopener noreferrer"&gt;parsing problems →&lt;/a&gt; when the interviewer wants a string-to-structure conversion.&lt;/li&gt;
&lt;li&gt;Sharpen &lt;a href="https://pipecode.ai/explore/practice/topic/data-transformation" rel="noopener noreferrer"&gt;data-transformation drills →&lt;/a&gt; for wide-to-long, long-to-wide, and JSON-to-relational reshapes.&lt;/li&gt;
&lt;li&gt;Layer the &lt;a href="https://pipecode.ai/explore/practice/topic/string-parsing" rel="noopener noreferrer"&gt;string-parsing library →&lt;/a&gt; for the tokenisation primitives that underpin every JSON path.&lt;/li&gt;
&lt;li&gt;Stack the &lt;a href="https://pipecode.ai/explore/practice/topic/data-manipulation" rel="noopener noreferrer"&gt;data manipulation library →&lt;/a&gt; for &lt;code&gt;UPDATE&lt;/code&gt;, &lt;code&gt;MERGE&lt;/code&gt;, and &lt;code&gt;jsonb_set&lt;/code&gt; interview probes.&lt;/li&gt;
&lt;li&gt;For general SQL sharpening, work through the &lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;SQL problem library →&lt;/a&gt; which contains 450+ DE-focused questions.&lt;/li&gt;
&lt;li&gt;For the broader SQL interview surface, take the &lt;a href="https://pipecode.ai/explore/courses/sql-for-data-engineering-interviews-from-zero-to-faang" rel="noopener noreferrer"&gt;SQL for Data Engineering course →&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/" rel="noopener noreferrer"&gt;Pipecode.ai&lt;/a&gt; is Leetcode for Data Engineering — every `sql json` recipe above ships with hands-on practice rooms where you write the Postgres `jsonb` GIN filter, wire the Snowflake `LATERAL FLATTEN`, chase the BigQuery native-JSON migration, and rehearse the `JSON_TABLE` interview walk-through against real graded inputs. PipeCode pairs every reading with 450+ DE-focused problems and a real-time scoring engine, so you never have to wonder whether your `json_value sql` answer holds up under a senior interviewer's depth probes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/json" rel="noopener noreferrer"&gt;Practice JSON patterns now →&lt;/a&gt;&lt;br&gt;
&lt;a href="https://pipecode.ai/explore/practice/topic/data-transformation" rel="noopener noreferrer"&gt;Data transformation drills →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>sql</category>
      <category>interview</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>SQL PIVOT / UNPIVOT / CROSSTAB Across Dialects (Postgres, Snowflake, BigQuery, SQL Server)</title>
      <dc:creator>Gowtham Potureddi</dc:creator>
      <pubDate>Fri, 10 Jul 2026 17:29:59 +0000</pubDate>
      <link>https://dev.to/gowthampotureddi/sql-pivot-unpivot-crosstab-across-dialects-postgres-snowflake-bigquery-sql-server-fg1</link>
      <guid>https://dev.to/gowthampotureddi/sql-pivot-unpivot-crosstab-across-dialects-postgres-snowflake-bigquery-sql-server-fg1</guid>
      <description>&lt;p&gt;&lt;strong&gt;&lt;code&gt;sql pivot&lt;/code&gt;&lt;/strong&gt; is the single most-searched reshaping keyword in analytics SQL — and the single most dialect-fractured one in 2026. The same "turn a long monthly-sales table into a wide one column per month" ask has four different answers across the four warehouses most data teams live in: Postgres uses &lt;code&gt;tablefunc.crosstab&lt;/code&gt;, Snowflake ships a full ANSI &lt;code&gt;PIVOT&lt;/code&gt; with a dynamic &lt;code&gt;ANY ORDER BY&lt;/code&gt; form, BigQuery ships &lt;code&gt;PIVOT&lt;/code&gt; as a table-function on a subquery, and SQL Server ships the OG &lt;code&gt;PIVOT&lt;/code&gt; with an alias table and bracketed labels. There is no portable spelling; the interviewer wants to hear you say that out loud in the first sentence.&lt;/p&gt;

&lt;p&gt;This guide is the mid-to-senior comparison you wished existed the first time an interviewer asked you to write &lt;code&gt;snowflake pivot&lt;/code&gt; on the whiteboard, sketch a &lt;code&gt;sql server pivot&lt;/code&gt; for a monthly reporting table, wrangle a &lt;code&gt;bigquery pivot&lt;/code&gt; for a survey dataset, run an &lt;code&gt;sql unpivot&lt;/code&gt; to normalise a wide extract, or roll a &lt;code&gt;dynamic pivot sql&lt;/code&gt; where the column set is unknown at compile time. It walks through the long-to-wide reshape via &lt;code&gt;postgres crosstab&lt;/code&gt; and PIVOT keywords (Postgres tablefunc + Snowflake / BigQuery / SQL Server PIVOT), the wide-to-long reshape via &lt;code&gt;UNPIVOT&lt;/code&gt; and its dialect equivalents (SQL Server / Snowflake / BigQuery UNPIVOT, Postgres via &lt;code&gt;UNION ALL&lt;/code&gt; / &lt;code&gt;VALUES&lt;/code&gt; / &lt;code&gt;jsonb_each_text&lt;/code&gt;, MySQL emulation), dynamic pivots when the column set is unknown at compile time (&lt;code&gt;PREPARE&lt;/code&gt; / &lt;code&gt;EXECUTE&lt;/code&gt;, Snowflake &lt;code&gt;ANY ORDER BY&lt;/code&gt;, dbt macros, Python-generated SQL), and the universal &lt;code&gt;SUM(CASE WHEN …)&lt;/code&gt; plus ANSI &lt;code&gt;FILTER (WHERE …)&lt;/code&gt; conditional-aggregation fallback that works in every dialect. Each section pairs a teaching block with a Solution-Tail interview answer — code, a step-by-step trace, an output table, then a concept-by-concept breakdown of why it works.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0h7ir1pb6epqiktya7i4.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0h7ir1pb6epqiktya7i4.jpeg" alt="PipeCode blog header for SQL PIVOT / UNPIVOT / CROSSTAB across dialects — bold white headline 'SQL PIVOT · UNPIVOT · CROSSTAB' with subtitle 'Postgres · Snowflake · BigQuery · SQL Server' and a stylised long-to-wide reshaping diagram on a dark gradient with purple, green, orange, and blue accents and a small pipecode.ai attribution." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you want &lt;strong&gt;hands-on reps&lt;/strong&gt; immediately after reading, drill the &lt;a href="https://pipecode.ai/explore/practice/topic/unpivoting" rel="noopener noreferrer"&gt;unpivoting practice library →&lt;/a&gt;, rehearse on &lt;a href="https://pipecode.ai/explore/practice/topic/conditional-aggregation" rel="noopener noreferrer"&gt;conditional-aggregation problems →&lt;/a&gt;, and sharpen the grouping axis with &lt;a href="https://pipecode.ai/explore/practice/topic/grouping-sets" rel="noopener noreferrer"&gt;grouping-sets drills →&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;On this page&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why pivot patterns matter in 2026&lt;/li&gt;
&lt;li&gt;Postgres crosstab + Snowflake / BigQuery PIVOT&lt;/li&gt;
&lt;li&gt;UNPIVOT and dialect equivalents&lt;/li&gt;
&lt;li&gt;Dynamic pivots — unknown columns at compile time&lt;/li&gt;
&lt;li&gt;Conditional-aggregation fallback&lt;/li&gt;
&lt;li&gt;Cheat sheet — pivot and unpivot recipes&lt;/li&gt;
&lt;li&gt;Frequently asked questions&lt;/li&gt;
&lt;li&gt;Practice on PipeCode&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1. Why pivot patterns matter in 2026
&lt;/h2&gt;

&lt;h3&gt;
  
  
  PIVOT and UNPIVOT are the two halves of table reshaping — every dialect spells them differently
&lt;/h3&gt;

&lt;p&gt;The one-sentence invariant: &lt;strong&gt;a &lt;code&gt;sql pivot&lt;/code&gt; turns a long-form table (one row per observation) into a wide-form table (one column per observation category), and a &lt;code&gt;sql unpivot&lt;/code&gt; reverses the operation — but no two major warehouses spell the operation the same way, so every senior data engineer keeps a mental dialect matrix in their head&lt;/strong&gt;. Once you internalise that "reshape direction + dialect keyword" is the whole design space, the entire pivot interview surface collapses to a lookup: what's the input shape, what's the target shape, and what's the warehouse?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Four axes interviewers actually probe.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Direction.&lt;/strong&gt; Long-to-wide (&lt;code&gt;PIVOT&lt;/code&gt;) or wide-to-long (&lt;code&gt;UNPIVOT&lt;/code&gt;). Analysts usually want long-to-wide for reporting; ETL pipelines usually want wide-to-long for normalisation. The mental model split is: &lt;strong&gt;PIVOT collapses rows into columns; UNPIVOT explodes columns into rows&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Known vs unknown column set.&lt;/strong&gt; If the labels are known at compile time (Jan, Feb, Mar), any dialect's &lt;code&gt;PIVOT&lt;/code&gt; handles it. If they are unknown at compile time (a new product category can arrive at any time), you need either &lt;code&gt;dynamic pivot sql&lt;/code&gt; (&lt;code&gt;PREPARE&lt;/code&gt; / &lt;code&gt;EXECUTE&lt;/code&gt;), Snowflake's &lt;code&gt;ANY ORDER BY&lt;/code&gt; form, or a code-generator (dbt macro, Python + SQLAlchemy).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aggregate function.&lt;/strong&gt; &lt;code&gt;PIVOT&lt;/code&gt; requires an aggregate — &lt;code&gt;SUM&lt;/code&gt;, &lt;code&gt;MAX&lt;/code&gt;, &lt;code&gt;MIN&lt;/code&gt;, &lt;code&gt;AVG&lt;/code&gt;, &lt;code&gt;COUNT&lt;/code&gt;, &lt;code&gt;LIST_AGG&lt;/code&gt;. The choice matters: &lt;code&gt;MAX&lt;/code&gt; vs &lt;code&gt;SUM&lt;/code&gt; produces different answers when there are duplicate rows. Interviewers love to probe "what if two rows share the same (product, month) key?" — the answer depends on the aggregate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Portability.&lt;/strong&gt; &lt;code&gt;SUM(CASE WHEN …)&lt;/code&gt; and the ANSI &lt;code&gt;FILTER (WHERE …)&lt;/code&gt; clause both work as universal fallbacks, and both plan identically to &lt;code&gt;PIVOT&lt;/code&gt; in every modern warehouse. When your team ships one query across Postgres + Snowflake + BigQuery, conditional aggregation is often the right call.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The dialect split — same shape, different keywords.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Postgres&lt;/strong&gt; — no native &lt;code&gt;PIVOT&lt;/code&gt; keyword; use the &lt;code&gt;tablefunc&lt;/code&gt; extension's &lt;code&gt;crosstab(text, text)&lt;/code&gt; function, or fall back to &lt;code&gt;SUM(CASE WHEN …)&lt;/code&gt;. The &lt;code&gt;crosstab&lt;/code&gt; function has known ergonomic warts (output column names must be pre-declared, category ordering matters).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snowflake&lt;/strong&gt; — full ANSI &lt;code&gt;PIVOT(agg FOR col IN (v1, v2, …))&lt;/code&gt; since 2019, plus a flagship &lt;code&gt;ANY ORDER BY&lt;/code&gt; dynamic form since 2023 that eliminates the need for &lt;code&gt;EXECUTE IMMEDIATE&lt;/code&gt; for most cases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BigQuery&lt;/strong&gt; — table-function &lt;code&gt;PIVOT(agg FOR col IN (v1, v2, …))&lt;/code&gt; applied to a subquery. Requires explicit column list; no &lt;code&gt;ANY&lt;/code&gt; form.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL Server&lt;/strong&gt; — the original &lt;code&gt;PIVOT(agg FOR col IN ([v1], [v2], …)) p&lt;/code&gt; syntax with an alias table. Labels are &lt;code&gt;[bracketed]&lt;/code&gt; because they become identifiers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MySQL / MariaDB&lt;/strong&gt; — no &lt;code&gt;PIVOT&lt;/code&gt; keyword at all. Every pivot is either &lt;code&gt;SUM(CASE WHEN …)&lt;/code&gt; or a stored procedure that builds dynamic SQL from a metadata query.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When conditional aggregation still wins over &lt;code&gt;PIVOT&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mixed aggregates in one row.&lt;/strong&gt; &lt;code&gt;SUM(CASE WHEN month = 'Jan' THEN sales END) AS jan_sales, COUNT(CASE WHEN month = 'Jan' THEN 1 END) AS jan_orders&lt;/code&gt;. &lt;code&gt;PIVOT&lt;/code&gt; gives you one aggregate for all columns; &lt;code&gt;CASE WHEN&lt;/code&gt; lets you mix &lt;code&gt;SUM&lt;/code&gt;, &lt;code&gt;COUNT&lt;/code&gt;, &lt;code&gt;MAX&lt;/code&gt;, and &lt;code&gt;AVG&lt;/code&gt; freely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tiny column sets.&lt;/strong&gt; If you have 3 pivoted columns, &lt;code&gt;SUM(CASE WHEN …)&lt;/code&gt; is 6 lines vs &lt;code&gt;PIVOT&lt;/code&gt;'s 4 lines and is instantly portable across dialects — no readability loss.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dialect portability.&lt;/strong&gt; One query that works identically on Postgres, Snowflake, BigQuery, and SQL Server. Conditional aggregation is the only mechanism that satisfies that constraint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debuggable.&lt;/strong&gt; The plan is a plain &lt;code&gt;GROUP BY&lt;/code&gt; with a projection list. Every runner explains it the same way. &lt;code&gt;PIVOT&lt;/code&gt; in some dialects hides the plan inside a syntactic sugar layer that is harder to read.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview asks.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Reshape a monthly sales table (year, month, product, sales) into a wide table with one column per month." — the classic long-to-wide.&lt;/li&gt;
&lt;li&gt;"Given a wide survey table (respondent_id, q1, q2, q3, q4, q5), turn it into a long (respondent_id, question, answer) shape." — the classic wide-to-long.&lt;/li&gt;
&lt;li&gt;"New product categories are added to a source table every week. Write a report that pivots by category — how do you handle the unknown column set?" — the &lt;code&gt;dynamic pivot sql&lt;/code&gt; probe.&lt;/li&gt;
&lt;li&gt;"Same reshape, but the pipeline runs on both Snowflake and Postgres. Write one query that works in both." — the portability probe (answer: &lt;code&gt;SUM(CASE WHEN …)&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;"What is the difference between &lt;code&gt;PIVOT&lt;/code&gt; in SQL Server and &lt;code&gt;crosstab&lt;/code&gt; in Postgres?" — the dialect-comparison probe.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What interviewers listen for.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do you say &lt;strong&gt;"PIVOT is long-to-wide, UNPIVOT is wide-to-long"&lt;/strong&gt; in the first sentence? — required answer.&lt;/li&gt;
&lt;li&gt;Do you mention &lt;strong&gt;"Postgres has no PIVOT keyword — use crosstab or CASE WHEN"&lt;/strong&gt; unprompted? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you push back on &lt;strong&gt;"just use PIVOT everywhere"&lt;/strong&gt; with "MySQL and Postgres have no first-class PIVOT"? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you reach for &lt;strong&gt;&lt;code&gt;SUM(CASE WHEN …)&lt;/code&gt;&lt;/strong&gt; as the portable fallback? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you know &lt;strong&gt;&lt;code&gt;FILTER (WHERE …)&lt;/code&gt;&lt;/strong&gt; as the ANSI cleaner form? — senior signal.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Worked example — same monthly-sales reshape, four dialects
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The classic reporting ask — turn a long &lt;code&gt;(year, month, product, sales)&lt;/code&gt; table into a wide table with one column per month per year — reads identically as an English requirement across every warehouse. The SQL to express it looks wildly different by dialect. Writing the same reshape four ways is the fastest way to build a mental dialect matrix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given a &lt;code&gt;sales&lt;/code&gt; table with columns &lt;code&gt;(year, month, product, sales)&lt;/code&gt;, write a query that produces one row per product with columns &lt;code&gt;jan&lt;/code&gt;, &lt;code&gt;feb&lt;/code&gt;, &lt;code&gt;mar&lt;/code&gt; containing the sum of sales for each month. Show the query in Postgres (crosstab), Snowflake, BigQuery, and SQL Server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;year&lt;/th&gt;
&lt;th&gt;month&lt;/th&gt;
&lt;th&gt;product&lt;/th&gt;
&lt;th&gt;sales&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2026&lt;/td&gt;
&lt;td&gt;Jan&lt;/td&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026&lt;/td&gt;
&lt;td&gt;Feb&lt;/td&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026&lt;/td&gt;
&lt;td&gt;Mar&lt;/td&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026&lt;/td&gt;
&lt;td&gt;Jan&lt;/td&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026&lt;/td&gt;
&lt;td&gt;Feb&lt;/td&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026&lt;/td&gt;
&lt;td&gt;Mar&lt;/td&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;110&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Postgres — tablefunc crosstab (extension required)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;tablefunc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;crosstab&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="err"&gt;$$&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sales&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;
     &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;
     &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;
     &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="err"&gt;$$&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Jan'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Feb'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Mar'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jan&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;feb&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mar&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Snowflake — native ANSI PIVOT&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;feb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mar&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;PIVOT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sales&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Jan'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Feb'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Mar'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;feb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mar&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- BigQuery — table-function PIVOT on a subquery&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;PIVOT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sales&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Jan'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Feb'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Mar'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- SQL Server — PIVOT with alias table and bracketed labels&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Jan&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Feb&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Mar&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;
&lt;span class="n"&gt;PIVOT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sales&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Jan&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Feb&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Mar&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Postgres's &lt;code&gt;tablefunc.crosstab&lt;/code&gt; takes two SQL strings: the first must return three columns &lt;code&gt;(row_key, category, value)&lt;/code&gt; and be &lt;code&gt;ORDER BY row_key, category&lt;/code&gt;; the second is a &lt;code&gt;VALUES&lt;/code&gt; list that pins the category order. The &lt;code&gt;AS ct(…)&lt;/code&gt; alias declares the output column names — the ergonomic wart is that you must type them.&lt;/li&gt;
&lt;li&gt;Snowflake's &lt;code&gt;PIVOT&lt;/code&gt; operates on a subquery (or table). Every column of the subquery that is NOT named in &lt;code&gt;FOR&lt;/code&gt; or aggregated is treated as an implicit grouping key. &lt;code&gt;('Jan', 'Feb', 'Mar')&lt;/code&gt; is a quoted-string label list — Snowflake infers the column names from those labels.&lt;/li&gt;
&lt;li&gt;BigQuery's &lt;code&gt;PIVOT&lt;/code&gt; is nearly identical to Snowflake's but requires the subquery form (&lt;code&gt;FROM (SELECT …)&lt;/code&gt;). The output column names are &lt;code&gt;Jan&lt;/code&gt;, &lt;code&gt;Feb&lt;/code&gt;, &lt;code&gt;Mar&lt;/code&gt; — no bracketing.&lt;/li&gt;
&lt;li&gt;SQL Server's &lt;code&gt;PIVOT&lt;/code&gt; also uses a subquery + alias table, but labels go in &lt;code&gt;[square brackets]&lt;/code&gt; because they become identifiers. Any label with a space or a reserved word MUST be bracketed.&lt;/li&gt;
&lt;li&gt;Every dialect's output is identical: one row per product, three columns per month, sums populated. The interviewer wants to see you write two or three of these from memory.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;product&lt;/th&gt;
&lt;th&gt;jan&lt;/th&gt;
&lt;th&gt;feb&lt;/th&gt;
&lt;th&gt;mar&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;td&gt;110&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For any pivot in a Snowflake or BigQuery pipeline, reach for native &lt;code&gt;PIVOT&lt;/code&gt; first — it is the most readable. For Postgres, use &lt;code&gt;crosstab&lt;/code&gt; only if you already have the extension; otherwise &lt;code&gt;SUM(CASE WHEN …)&lt;/code&gt; is cleaner. For SQL Server, always bracket the labels even when they don't strictly require it — future you will insert a label with a space and you will be glad.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — direction reversal (wide-to-long)
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The mirror ask: given a wide table with one column per month, turn it into a long shape with &lt;code&gt;(product, month, sales)&lt;/code&gt;. The interviewer often frames this as "unpivot," "normalise," or "stack." Every dialect except MySQL has an &lt;code&gt;UNPIVOT&lt;/code&gt; keyword; the shapes differ subtly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given a wide &lt;code&gt;sales_wide&lt;/code&gt; table with columns &lt;code&gt;(product, jan, feb, mar)&lt;/code&gt;, write the wide-to-long reshape to &lt;code&gt;(product, month, sales)&lt;/code&gt; in Snowflake, BigQuery, SQL Server, and Postgres.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;product&lt;/th&gt;
&lt;th&gt;jan&lt;/th&gt;
&lt;th&gt;feb&lt;/th&gt;
&lt;th&gt;mar&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;td&gt;110&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Snowflake — UNPIVOT keyword&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales_wide&lt;/span&gt;
&lt;span class="n"&gt;UNPIVOT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;feb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mar&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- BigQuery — UNPIVOT keyword (same shape)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales_wide&lt;/span&gt;
&lt;span class="n"&gt;UNPIVOT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;feb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mar&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- SQL Server — UNPIVOT keyword, requires alias table&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;feb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mar&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales_wide&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;
&lt;span class="n"&gt;UNPIVOT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;feb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mar&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Postgres — no UNPIVOT keyword; use UNION ALL&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'jan'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jan&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales_wide&lt;/span&gt;
&lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'feb'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;feb&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales_wide&lt;/span&gt;
&lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'mar'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mar&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales_wide&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Snowflake's &lt;code&gt;UNPIVOT(sales FOR month IN (jan, feb, mar))&lt;/code&gt; reads as: "emit one row per input row per named column; put the column name in &lt;code&gt;month&lt;/code&gt; and the value in &lt;code&gt;sales&lt;/code&gt;." No aggregate is needed because unpivoting is a 1-to-N explosion.&lt;/li&gt;
&lt;li&gt;BigQuery's &lt;code&gt;UNPIVOT&lt;/code&gt; is syntactically identical. The one option worth knowing is &lt;code&gt;INCLUDE NULLS&lt;/code&gt; — by default &lt;code&gt;UNPIVOT&lt;/code&gt; drops rows where the unpivoted value is &lt;code&gt;NULL&lt;/code&gt;; adding &lt;code&gt;INCLUDE NULLS&lt;/code&gt; keeps them.&lt;/li&gt;
&lt;li&gt;SQL Server also uses &lt;code&gt;UNPIVOT&lt;/code&gt;, with an alias table like &lt;code&gt;PIVOT&lt;/code&gt;. Labels don't need brackets here because you're using them as column references, not identifier literals.&lt;/li&gt;
&lt;li&gt;Postgres has no &lt;code&gt;UNPIVOT&lt;/code&gt; keyword. The idiomatic fallback is &lt;code&gt;UNION ALL&lt;/code&gt; — one &lt;code&gt;SELECT&lt;/code&gt; per unpivoted column. This is the most explicit form: you name the month label and the value column at every branch.&lt;/li&gt;
&lt;li&gt;The output shape is identical in every dialect: &lt;code&gt;(product, month, sales)&lt;/code&gt; in long form. &lt;code&gt;UNPIVOT&lt;/code&gt; is the reverse operation of &lt;code&gt;PIVOT&lt;/code&gt; — apply both and you get back the original table.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;product&lt;/th&gt;
&lt;th&gt;month&lt;/th&gt;
&lt;th&gt;sales&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;jan&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;feb&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;mar&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;jan&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;feb&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;mar&lt;/td&gt;
&lt;td&gt;110&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For 3–5 columns to unpivot, &lt;code&gt;UNION ALL&lt;/code&gt; is fine even on Snowflake or SQL Server. For 20+ columns, always use &lt;code&gt;UNPIVOT&lt;/code&gt; (or Postgres's &lt;code&gt;jsonb_each_text&lt;/code&gt; trick) — the boilerplate savings pay for the marginal complexity.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the aggregate function matters
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A common interview trap: your input has multiple rows per &lt;code&gt;(product, month)&lt;/code&gt; because the source is a transaction log, not a monthly total. The &lt;code&gt;PIVOT&lt;/code&gt; aggregate matters — &lt;code&gt;SUM&lt;/code&gt; gives you totals, &lt;code&gt;MAX&lt;/code&gt; gives you the largest single transaction, &lt;code&gt;COUNT&lt;/code&gt; gives you the number of transactions. The interviewer wants to see you name the aggregate deliberately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; A &lt;code&gt;transactions&lt;/code&gt; table has multiple rows per &lt;code&gt;(product, month)&lt;/code&gt; — one per transaction. Write a &lt;code&gt;PIVOT&lt;/code&gt; that reports monthly sums, then modify it to report monthly transaction counts, then modify it to report the maximum single transaction per month. Use Snowflake syntax.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;product&lt;/th&gt;
&lt;th&gt;month&lt;/th&gt;
&lt;th&gt;amount&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;Jan&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;Jan&lt;/td&gt;
&lt;td&gt;70&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;Feb&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;Jan&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;Feb&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;Feb&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Monthly sum per product&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;transactions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;PIVOT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Jan'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Feb'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;-- Monthly transaction count per product&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;transactions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;PIVOT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Jan'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Feb'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;-- Monthly max single transaction per product&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;transactions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;PIVOT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Jan'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Feb'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;SUM(amount)&lt;/code&gt; treats the input as monthly totals to be aggregated. Widget in Jan has two rows (30 + 70), so the pivoted &lt;code&gt;'Jan'&lt;/code&gt; column for Widget shows 100.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;COUNT(amount)&lt;/code&gt; counts rows per &lt;code&gt;(product, month)&lt;/code&gt;. Widget in Jan again has two rows, so the pivoted &lt;code&gt;'Jan'&lt;/code&gt; column for Widget shows 2 (not 100).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MAX(amount)&lt;/code&gt; picks the largest single value per group. Widget in Jan has rows (30, 70), so the pivoted &lt;code&gt;'Jan'&lt;/code&gt; column for Widget shows 70.&lt;/li&gt;
&lt;li&gt;The interviewer wants to see you name the aggregate deliberately. "I'm using SUM because we want monthly totals; if we wanted transaction counts, I'd swap to COUNT" is the answer that shows depth.&lt;/li&gt;
&lt;li&gt;Every dialect's &lt;code&gt;PIVOT&lt;/code&gt; requires exactly one aggregate function. To emit both sum and count in the same query, you need conditional aggregation (&lt;code&gt;SUM(CASE WHEN month = 'Jan' THEN amount END) AS jan_sum, COUNT(CASE WHEN month = 'Jan' THEN 1 END) AS jan_count&lt;/code&gt;) — which is why the conditional-aggregation fallback is the senior go-to for mixed aggregates.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output (SUM variant).&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;product&lt;/th&gt;
&lt;th&gt;Jan&lt;/th&gt;
&lt;th&gt;Feb&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Always name the aggregate function in the first sentence of your interview answer: "I'll use &lt;code&gt;SUM&lt;/code&gt; because we want monthly totals." When the interviewer asks a follow-up ("what if we wanted counts?"), swap &lt;code&gt;SUM&lt;/code&gt; to &lt;code&gt;COUNT&lt;/code&gt; — the shape doesn't change, only the aggregate does.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on pivot / unpivot mental model
&lt;/h3&gt;

&lt;p&gt;A senior interviewer often opens with: "Walk me through when you reach for &lt;code&gt;PIVOT&lt;/code&gt; vs &lt;code&gt;UNPIVOT&lt;/code&gt; vs &lt;code&gt;SUM(CASE WHEN …)&lt;/code&gt;. What is the mental split, and when do you pick one over the others?"&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a 4-question decision framework
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Decision framework — PIVOT / UNPIVOT / conditional aggregation

1. Direction?
   - Long-to-wide  → PIVOT (or SUM CASE WHEN as portable fallback)
   - Wide-to-long  → UNPIVOT (or UNION ALL as portable fallback)

2. Column set known at compile time?
   - Yes → static PIVOT / UNPIVOT
   - No  → dynamic PIVOT (Snowflake ANY ORDER BY, or EXECUTE IMMEDIATE)

3. Single aggregate or mixed?
   - Single (all SUM, all COUNT, all MAX) → PIVOT
   - Mixed (SUM + COUNT + MAX in one row) → conditional aggregation

4. Portability required?
   - One dialect         → native PIVOT / UNPIVOT
   - Multiple dialects   → conditional aggregation (works everywhere)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ask&lt;/th&gt;
&lt;th&gt;Q1 direction&lt;/th&gt;
&lt;th&gt;Q2 known cols&lt;/th&gt;
&lt;th&gt;Q3 mixed&lt;/th&gt;
&lt;th&gt;Q4 portable&lt;/th&gt;
&lt;th&gt;Picked&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Monthly sales matrix (Snowflake only)&lt;/td&gt;
&lt;td&gt;long-to-wide&lt;/td&gt;
&lt;td&gt;yes (12 months)&lt;/td&gt;
&lt;td&gt;single SUM&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;Snowflake PIVOT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monthly matrix (Postgres + Snowflake)&lt;/td&gt;
&lt;td&gt;long-to-wide&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;single SUM&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;SUM(CASE WHEN …)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Report SUM + COUNT + MAX per month&lt;/td&gt;
&lt;td&gt;long-to-wide&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;mixed&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;conditional aggregation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New product category added weekly&lt;/td&gt;
&lt;td&gt;long-to-wide&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;single&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;Snowflake ANY ORDER BY&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wide survey (q1..q10) to long&lt;/td&gt;
&lt;td&gt;wide-to-long&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;UNPIVOT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wide survey, portable across warehouses&lt;/td&gt;
&lt;td&gt;wide-to-long&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;UNION ALL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After the 4-question pass, the reshape strategy is unambiguous. The remaining 5% — where multiple answers work — defaults to whatever the team's style guide already picks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;When it wins&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Native PIVOT&lt;/td&gt;
&lt;td&gt;Single dialect, known columns, single aggregate, best readability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snowflake ANY ORDER BY&lt;/td&gt;
&lt;td&gt;Snowflake, unknown columns, no need to write dynamic SQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SUM(CASE WHEN …)&lt;/td&gt;
&lt;td&gt;Multi-dialect portability, mixed aggregates, tiny column sets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UNPIVOT&lt;/td&gt;
&lt;td&gt;Wide-to-long, single dialect, known columns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UNION ALL&lt;/td&gt;
&lt;td&gt;Wide-to-long, Postgres or MySQL, or portability required&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Direction is the first split&lt;/strong&gt;&lt;/strong&gt; — PIVOT and UNPIVOT are not variants; they are opposite operations. Every reshape either compresses rows into columns (PIVOT) or explodes columns into rows (UNPIVOT). Answering the direction question first eliminates half of the option space.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Known vs unknown columns&lt;/strong&gt;&lt;/strong&gt; — the column set determines whether static SQL is enough or whether you need a code-generation layer. Interviewers care about this because it separates "reads docs" from "designs pipelines that survive schema drift."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Single vs mixed aggregates&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;PIVOT&lt;/code&gt; gives you one aggregate for every output column; conditional aggregation lets you mix. This is the axis that decides whether the natural syntax is &lt;code&gt;PIVOT&lt;/code&gt; or &lt;code&gt;SUM(CASE WHEN …)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Portability is a hard constraint&lt;/strong&gt;&lt;/strong&gt; — one query that runs on Postgres AND Snowflake AND BigQuery cannot use native &lt;code&gt;PIVOT&lt;/code&gt; in Postgres. Conditional aggregation is the only mechanism that satisfies "runs everywhere without translation."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — every strategy plans to the same &lt;code&gt;GROUP BY&lt;/code&gt; scan under the hood. The choice is a readability / portability trade-off, not a performance one. Pick the shape that reads best in your codebase's dialect.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — unpivoting&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Unpivot / reshape problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/unpivoting" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — conditional aggregation&lt;/span&gt;
&lt;strong&gt;Conditional aggregation problems&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/conditional-aggregation" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  2. Postgres crosstab + Snowflake / BigQuery PIVOT
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;postgres crosstab&lt;/code&gt; is the tablefunc form; &lt;code&gt;snowflake pivot&lt;/code&gt; and &lt;code&gt;bigquery pivot&lt;/code&gt; ship the ANSI PIVOT keyword; &lt;code&gt;sql server pivot&lt;/code&gt; is the OG dialect ancestor
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;Postgres has no PIVOT keyword — it exposes the &lt;code&gt;tablefunc.crosstab&lt;/code&gt; function; Snowflake, BigQuery, and SQL Server all ship an ANSI-style &lt;code&gt;PIVOT&lt;/code&gt; clause, but the label syntax and required aliases differ&lt;/strong&gt;. Once you say "Postgres uses crosstab, everyone else uses PIVOT with different label syntaxes," the entire long-to-wide interview surface becomes a syntax lookup rather than a design problem.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F18as6ksyh3o7x0tgirkz.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F18as6ksyh3o7x0tgirkz.jpeg" alt="Visual dialect-matrix of PIVOT syntax across Postgres, Snowflake, BigQuery, and SQL Server — four columns showing the same monthly-sales reshape written four ways; a support matrix at the bottom clarifying which dialects have native PIVOT and which fall back to crosstab or CASE WHEN; on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Postgres tablefunc.crosstab — the two-argument form.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CREATE EXTENSION IF NOT EXISTS tablefunc;&lt;/code&gt; — one-time install per database.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;crosstab('SELECT row_key, category, value FROM t ORDER BY 1, 2', 'VALUES (''a''), (''b''), (''c'')')&lt;/code&gt; — first arg produces the long-form input, second arg pins the category order.&lt;/li&gt;
&lt;li&gt;Output column list must be declared in &lt;code&gt;AS ct(row_key type, cat_a type, cat_b type, cat_c type)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Cardinality guarantee: category order in output = row order in the second &lt;code&gt;VALUES&lt;/code&gt; list.&lt;/li&gt;
&lt;li&gt;Common gotcha: the first argument MUST be &lt;code&gt;ORDER BY row_key, category&lt;/code&gt; — otherwise crosstab silently returns wrong results.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Postgres tablefunc.crosstab — the single-argument form.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;crosstab('SELECT row_key, category, value FROM t ORDER BY 1, 2')&lt;/code&gt; — automatic category detection, but the first N distinct categories in the ordered input become the columns.&lt;/li&gt;
&lt;li&gt;Rarely the right choice — you almost always want to name the categories explicitly to make ordering and missing values deterministic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Snowflake PIVOT.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;SELECT * FROM t PIVOT(SUM(sales) FOR month IN ('Jan', 'Feb', 'Mar'))&lt;/code&gt; — quoted string labels; the output column names are &lt;code&gt;'Jan'&lt;/code&gt;, &lt;code&gt;'Feb'&lt;/code&gt;, &lt;code&gt;'Mar'&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The IN clause supports comma-separated literal values, a subquery (&lt;code&gt;FOR month IN (SELECT DISTINCT month FROM t)&lt;/code&gt;), or the flagship &lt;code&gt;ANY ORDER BY month&lt;/code&gt; dynamic form (Snowflake exclusive, GA 2023).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DEFAULT ON NULL (0)&lt;/code&gt; option — Snowflake-specific extension that replaces &lt;code&gt;NULL&lt;/code&gt; cells with a default value.&lt;/li&gt;
&lt;li&gt;Aliasing: &lt;code&gt;PIVOT(…) AS p (product, jan, feb, mar)&lt;/code&gt; renames both the implicit group columns and the pivoted columns.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;BigQuery PIVOT.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;SELECT * FROM (SELECT product, month, sales FROM t) PIVOT(SUM(sales) FOR month IN ('Jan', 'Feb', 'Mar'))&lt;/code&gt; — mandatory subquery form.&lt;/li&gt;
&lt;li&gt;Output column names are the literal string values without quotes (&lt;code&gt;Jan&lt;/code&gt;, &lt;code&gt;Feb&lt;/code&gt;, &lt;code&gt;Mar&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Supports &lt;code&gt;AS alias&lt;/code&gt; after each &lt;code&gt;IN&lt;/code&gt; value: &lt;code&gt;IN ('Jan' AS jan_2026, 'Feb' AS feb_2026)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;No &lt;code&gt;ANY&lt;/code&gt; form — the label list is always static. Dynamic pivots require &lt;code&gt;EXECUTE IMMEDIATE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Non-string labels (integers, dates) work but produce numeric column names like &lt;code&gt;2026&lt;/code&gt;, which must be back-ticked in downstream queries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;SQL Server PIVOT.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;SELECT product, [Jan], [Feb] FROM (SELECT product, month, sales FROM t) src PIVOT(SUM(sales) FOR month IN ([Jan], [Feb])) AS p&lt;/code&gt; — bracketed labels because they become identifiers.&lt;/li&gt;
&lt;li&gt;Alias table (&lt;code&gt;AS p&lt;/code&gt;) is required; the &lt;code&gt;src&lt;/code&gt; alias on the inner subquery is required.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;NULL&lt;/code&gt; value is displayed as &lt;code&gt;NULL&lt;/code&gt; — no default-value option; use &lt;code&gt;COALESCE&lt;/code&gt; on the outer projection.&lt;/li&gt;
&lt;li&gt;Labels with spaces or reserved words MUST be bracketed; even without special characters, brackets are the safe default.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The 4-dialect matrix at a glance.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Postgres:&lt;/strong&gt; &lt;code&gt;crosstab&lt;/code&gt; function + &lt;code&gt;AS ct(…)&lt;/code&gt; declared output types.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snowflake:&lt;/strong&gt; &lt;code&gt;PIVOT(agg FOR col IN ('a', 'b'))&lt;/code&gt; + optional &lt;code&gt;ANY ORDER BY&lt;/code&gt; for dynamic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BigQuery:&lt;/strong&gt; &lt;code&gt;PIVOT(agg FOR col IN ('a', 'b'))&lt;/code&gt; on subquery + no &lt;code&gt;ANY&lt;/code&gt; form.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL Server:&lt;/strong&gt; &lt;code&gt;PIVOT(agg FOR col IN ([a], [b])) AS p&lt;/code&gt; + bracketed labels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MySQL / MariaDB:&lt;/strong&gt; no keyword; use &lt;code&gt;SUM(CASE WHEN …)&lt;/code&gt; or build dynamic SQL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on PIVOT syntax.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"How does Postgres do PIVOT?" — it doesn't; use &lt;code&gt;tablefunc.crosstab&lt;/code&gt; or &lt;code&gt;SUM(CASE WHEN …)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;"What is the second argument of &lt;code&gt;crosstab&lt;/code&gt;?" — the category order pin list; ensures deterministic column order.&lt;/li&gt;
&lt;li&gt;"What is &lt;code&gt;ANY ORDER BY&lt;/code&gt; in Snowflake PIVOT?" — dynamic pivot where the label set is discovered at query time; no dynamic SQL required.&lt;/li&gt;
&lt;li&gt;"Does BigQuery support dynamic PIVOT?" — not natively; use &lt;code&gt;EXECUTE IMMEDIATE&lt;/code&gt; on a string-built column list.&lt;/li&gt;
&lt;li&gt;"Why does SQL Server PIVOT need brackets on labels?" — labels become identifiers; brackets escape reserved words and spaces.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — Postgres tablefunc crosstab, two-argument form
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The most common &lt;code&gt;postgres crosstab&lt;/code&gt; interview question: given a sparse &lt;code&gt;(product, month, sales)&lt;/code&gt; table, reshape to a wide monthly matrix. Some product-month cells have no rows — the interviewer wants to know that the two-argument form handles missing cells cleanly, and that you know to &lt;code&gt;ORDER BY 1, 2&lt;/code&gt; in the first argument.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given a &lt;code&gt;sales&lt;/code&gt; table with sparse &lt;code&gt;(product, month, sales)&lt;/code&gt; rows, write a Postgres query using &lt;code&gt;tablefunc.crosstab&lt;/code&gt; that produces one row per product with columns &lt;code&gt;jan&lt;/code&gt;, &lt;code&gt;feb&lt;/code&gt;, &lt;code&gt;mar&lt;/code&gt;. Explain what happens for missing product-month combinations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;product&lt;/th&gt;
&lt;th&gt;month&lt;/th&gt;
&lt;th&gt;sales&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;Jan&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;Mar&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;Feb&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;tablefunc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;crosstab&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="err"&gt;$$&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sales&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;
    &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
  &lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="err"&gt;$$&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Jan'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Feb'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Mar'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;jan&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;feb&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;mar&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The first argument must produce three columns: the row key (&lt;code&gt;product&lt;/code&gt;), the category (&lt;code&gt;month&lt;/code&gt;), and the value (&lt;code&gt;SUM(sales)&lt;/code&gt;). It must be &lt;code&gt;ORDER BY row_key, category&lt;/code&gt; — otherwise crosstab may assign values to the wrong output cells.&lt;/li&gt;
&lt;li&gt;The second argument is a &lt;code&gt;VALUES&lt;/code&gt; list that pins the output column order. &lt;code&gt;'Jan'&lt;/code&gt; is column 1, &lt;code&gt;'Feb'&lt;/code&gt; is column 2, &lt;code&gt;'Mar'&lt;/code&gt; is column 3. Categories in the first arg not present in this list are dropped from the output.&lt;/li&gt;
&lt;li&gt;Missing product-month combinations produce &lt;code&gt;NULL&lt;/code&gt; in the output cell. Widget has no February row, so &lt;code&gt;feb&lt;/code&gt; is &lt;code&gt;NULL&lt;/code&gt; for Widget. To display 0 instead, wrap the outer projection with &lt;code&gt;COALESCE(jan, 0)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;AS ct(…)&lt;/code&gt; alias declares the output column names AND their types. You cannot omit this — crosstab is a &lt;code&gt;SETOF record&lt;/code&gt; function, and Postgres requires explicit type declarations for record-returning functions in the FROM clause.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;::int&lt;/code&gt; cast on &lt;code&gt;SUM(sales)&lt;/code&gt; matters because &lt;code&gt;SUM&lt;/code&gt; returns &lt;code&gt;bigint&lt;/code&gt; by default. The output column types in &lt;code&gt;AS ct(…)&lt;/code&gt; must match — a mismatch throws &lt;code&gt;structure of query does not match function result type&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;product&lt;/th&gt;
&lt;th&gt;jan&lt;/th&gt;
&lt;th&gt;feb&lt;/th&gt;
&lt;th&gt;mar&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Always use the two-argument form of &lt;code&gt;crosstab&lt;/code&gt;. The one-argument form auto-detects categories from the input, which makes column order non-deterministic and breaks when a category is missing from a batch. Pin the categories explicitly with &lt;code&gt;VALUES (...)&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — Snowflake PIVOT with an inline subquery
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Snowflake's &lt;code&gt;PIVOT&lt;/code&gt; is the cleanest of the four — no alias table required, no bracketing, and the operand can be an inline subquery. The interviewer often uses this to test whether you understand implicit grouping: any column in the input NOT named in &lt;code&gt;FOR&lt;/code&gt; and NOT aggregated becomes an implicit &lt;code&gt;GROUP BY&lt;/code&gt; key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given a &lt;code&gt;orders&lt;/code&gt; table with &lt;code&gt;(order_id, product, region, month, amount)&lt;/code&gt;, write a Snowflake &lt;code&gt;PIVOT&lt;/code&gt; that produces monthly sums per &lt;code&gt;(product, region)&lt;/code&gt;. Explain the implicit grouping and how to avoid an accidental cross-product from extra columns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;order_id&lt;/th&gt;
&lt;th&gt;product&lt;/th&gt;
&lt;th&gt;region&lt;/th&gt;
&lt;th&gt;month&lt;/th&gt;
&lt;th&gt;amount&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;US&lt;/td&gt;
&lt;td&gt;Jan&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;US&lt;/td&gt;
&lt;td&gt;Feb&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;EU&lt;/td&gt;
&lt;td&gt;Jan&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;US&lt;/td&gt;
&lt;td&gt;Jan&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Correct — implicit grouping on (product, region)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;PIVOT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Jan'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Feb'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;feb&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- WRONG — order_id makes every row unique, defeats the pivot&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="n"&gt;PIVOT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Jan'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Feb'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;PIVOT&lt;/code&gt; treats every column of the input NOT named in &lt;code&gt;FOR&lt;/code&gt; and NOT the aggregated column as an implicit &lt;code&gt;GROUP BY&lt;/code&gt; key. In the correct version, &lt;code&gt;product&lt;/code&gt; and &lt;code&gt;region&lt;/code&gt; are the implicit groups.&lt;/li&gt;
&lt;li&gt;In the wrong version, &lt;code&gt;order_id&lt;/code&gt; is unique per row — so the implicit &lt;code&gt;GROUP BY (order_id, product, region)&lt;/code&gt; produces one output row per input row. The pivot degenerates because every group has exactly one &lt;code&gt;(product, region, month)&lt;/code&gt; combination.&lt;/li&gt;
&lt;li&gt;Always project only the columns you want as grouping keys + the pivot column + the value column, using an inner subquery: &lt;code&gt;SELECT product, region, month, amount FROM orders&lt;/code&gt;. Never let PIVOT operate on a table with extra columns you don't want as grouping keys.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;AS p (product, region, jan, feb)&lt;/code&gt; clause renames the implicit and explicit columns in one shot. It is optional — without it, Snowflake keeps the implicit column names and generates &lt;code&gt;'Jan'&lt;/code&gt;, &lt;code&gt;'Feb'&lt;/code&gt; as the pivoted column names.&lt;/li&gt;
&lt;li&gt;To sanity-check, run the subquery first: &lt;code&gt;SELECT product, region, month, amount FROM orders&lt;/code&gt;. If those four columns are exactly what you want to pivot, wrap with &lt;code&gt;PIVOT&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;product&lt;/th&gt;
&lt;th&gt;region&lt;/th&gt;
&lt;th&gt;jan&lt;/th&gt;
&lt;th&gt;feb&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;US&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;EU&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;US&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Always use the inline-subquery form of &lt;code&gt;PIVOT&lt;/code&gt;. Never point &lt;code&gt;PIVOT&lt;/code&gt; at a raw table — the implicit &lt;code&gt;GROUP BY&lt;/code&gt; on every column is the most common &lt;code&gt;snowflake pivot&lt;/code&gt; bug. The subquery gives you control over exactly which columns become grouping keys.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — BigQuery PIVOT with column aliases
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; BigQuery's &lt;code&gt;PIVOT&lt;/code&gt; produces output column names from the label literals verbatim. &lt;code&gt;'Jan'&lt;/code&gt; becomes a column literally named &lt;code&gt;Jan&lt;/code&gt;, which is fine for identifiers but breaks when the label is a number, a date, or contains special characters. The &lt;code&gt;AS&lt;/code&gt; alias in the &lt;code&gt;IN&lt;/code&gt; list is the fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given an &lt;code&gt;events&lt;/code&gt; table with &lt;code&gt;(user_id, year, event_count)&lt;/code&gt;, write a BigQuery &lt;code&gt;PIVOT&lt;/code&gt; that produces one column per year (2024, 2025, 2026). Handle the fact that numeric column names must be back-ticked downstream.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;year&lt;/th&gt;
&lt;th&gt;event_count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2024&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2025&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;2026&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;2025&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;2026&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Without alias — numeric column names, requires back-ticks downstream&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_count&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;PIVOT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="nb"&gt;year&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2025&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;-- With alias — human-friendly column names&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_count&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;PIVOT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="nb"&gt;year&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="mi"&gt;2024&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;y2024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="mi"&gt;2025&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;y2025&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="mi"&gt;2026&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;y2026&lt;/span&gt;
&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Without the &lt;code&gt;AS&lt;/code&gt; alias, BigQuery produces columns literally named &lt;code&gt;2024&lt;/code&gt;, &lt;code&gt;2025&lt;/code&gt;, &lt;code&gt;2026&lt;/code&gt;. In subsequent queries, you must back-tick them: &lt;code&gt;SELECT \&lt;/code&gt;2024&lt;code&gt;FROM pivoted_table&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Back-ticked numeric column names are legal BigQuery but visually noisy and error-prone. Downstream tooling (Looker, dbt) often chokes on them.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;AS y2024&lt;/code&gt; alias in the &lt;code&gt;IN&lt;/code&gt; clause makes the output column name a plain identifier. This is the recommended pattern for any non-string label (numbers, dates, quoted strings with special characters).&lt;/li&gt;
&lt;li&gt;The same trick works in Snowflake: &lt;code&gt;IN (2024 AS y2024, 2025 AS y2025)&lt;/code&gt;. SQL Server doesn't need it because bracket labels are already identifier-safe.&lt;/li&gt;
&lt;li&gt;Postgres crosstab dodges this whole class by making you declare column names explicitly in &lt;code&gt;AS ct(user_id text, y2024 int, y2025 int, y2026 int)&lt;/code&gt; — the trade-off is verbosity in exchange for total control.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;y2024&lt;/th&gt;
&lt;th&gt;y2025&lt;/th&gt;
&lt;th&gt;y2026&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u1&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u2&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Any time your &lt;code&gt;PIVOT&lt;/code&gt; labels are not plain string identifiers, alias them with &lt;code&gt;AS&lt;/code&gt;. Numeric years, dates, quoted strings with hyphens, and anything else non-alphanumeric will produce hard-to-use column names downstream. The alias costs three characters and buys you a downstream identifier that behaves like every other column.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — SQL Server PIVOT with COALESCE and bracketed labels
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; SQL Server's &lt;code&gt;PIVOT&lt;/code&gt; produces &lt;code&gt;NULL&lt;/code&gt; for missing cells and has no built-in default-value option. The idiomatic fix is to wrap the outer projection with &lt;code&gt;COALESCE&lt;/code&gt;. The interviewer wants to see you handle NULL cells explicitly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given a &lt;code&gt;sales&lt;/code&gt; table with &lt;code&gt;(product, month, sales)&lt;/code&gt;, write a SQL Server &lt;code&gt;PIVOT&lt;/code&gt; for the three months (Jan, Feb, Mar) that reports &lt;code&gt;0&lt;/code&gt; for missing months. Show the alias-table + bracket syntax.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;product&lt;/th&gt;
&lt;th&gt;month&lt;/th&gt;
&lt;th&gt;sales&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;Jan&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;Feb&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;Jan&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Jan&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;jan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Feb&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;feb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Mar&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;mar&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;
&lt;span class="n"&gt;PIVOT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sales&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Jan&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Feb&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Mar&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The inner subquery (&lt;code&gt;src&lt;/code&gt;) alias is mandatory in SQL Server &lt;code&gt;PIVOT&lt;/code&gt; — the parser requires it.&lt;/li&gt;
&lt;li&gt;The alias table &lt;code&gt;AS p&lt;/code&gt; after the &lt;code&gt;PIVOT&lt;/code&gt; clause is also mandatory — SQL Server does not accept an unaliased pivot.&lt;/li&gt;
&lt;li&gt;Labels in the &lt;code&gt;IN&lt;/code&gt; clause are bracketed: &lt;code&gt;[Jan]&lt;/code&gt;, &lt;code&gt;[Feb]&lt;/code&gt;, &lt;code&gt;[Mar]&lt;/code&gt;. Brackets make them identifier-safe; without them, any label with a space or reserved word would break.&lt;/li&gt;
&lt;li&gt;Cells with no matching input rows come out as &lt;code&gt;NULL&lt;/code&gt;. &lt;code&gt;COALESCE([Jan], 0)&lt;/code&gt; in the outer projection replaces &lt;code&gt;NULL&lt;/code&gt; with &lt;code&gt;0&lt;/code&gt;. This is the SQL Server-idiomatic way — there is no &lt;code&gt;DEFAULT ON NULL&lt;/code&gt; clause like Snowflake.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;AS jan&lt;/code&gt; on each COALESCE renames the output column from &lt;code&gt;[Jan]&lt;/code&gt; (bracketed identifier) to &lt;code&gt;jan&lt;/code&gt; (plain identifier). This is optional but recommended — downstream tooling reads plain names more easily.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;product&lt;/th&gt;
&lt;th&gt;jan&lt;/th&gt;
&lt;th&gt;feb&lt;/th&gt;
&lt;th&gt;mar&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Always wrap &lt;code&gt;sql server pivot&lt;/code&gt; output with &lt;code&gt;COALESCE(col, 0)&lt;/code&gt; or &lt;code&gt;COALESCE(col, '')&lt;/code&gt; on the outer projection when the schema expects zero-instead-of-NULL. Do it in the outer &lt;code&gt;SELECT&lt;/code&gt;, not inside the pivot — the pivot itself has no NULL-substitution mechanism.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on Postgres crosstab specifics
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "You work on Postgres. You have a &lt;code&gt;daily_metrics&lt;/code&gt; table with (&lt;code&gt;date&lt;/code&gt;, &lt;code&gt;metric_name&lt;/code&gt;, &lt;code&gt;value&lt;/code&gt;) and you want a wide report with one column per metric. Walk me through the &lt;code&gt;crosstab&lt;/code&gt; call and the two common pitfalls people hit."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using tablefunc.crosstab with explicit category order
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Postgres tablefunc.crosstab — canonical pattern&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;tablefunc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;crosstab&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="err"&gt;$$&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;metric_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;daily_metrics&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;CURRENT_DATE&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'7 days'&lt;/span&gt;
    &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;metric_name&lt;/span&gt;       &lt;span class="c1"&gt;-- MUST be ordered&lt;/span&gt;
  &lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="err"&gt;$$&lt;/span&gt;
    &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'cpu_pct'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'mem_mb'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'disk_pct'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'conn_count'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;cpu_pct&lt;/span&gt;   &lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;mem_mb&lt;/span&gt;    &lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;disk_pct&lt;/span&gt;  &lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;conn_count&lt;/span&gt; &lt;span class="nb"&gt;numeric&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;First arg runs, returns rows &lt;code&gt;(date, metric, value)&lt;/code&gt; ordered by &lt;code&gt;(date, metric)&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;crosstab expects the natural row order&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Second arg establishes column order: cpu_pct, mem_mb, disk_pct, conn_count&lt;/td&gt;
&lt;td&gt;Pins output shape; missing metrics → NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;For each distinct &lt;code&gt;date&lt;/code&gt;, one output row is emitted&lt;/td&gt;
&lt;td&gt;Each row_key becomes one output row&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Each metric's &lt;code&gt;value&lt;/code&gt; lands in its matching column&lt;/td&gt;
&lt;td&gt;Cell = the value where &lt;code&gt;metric_name&lt;/code&gt; matches the category&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;AS ct(…) declares the column list + types&lt;/td&gt;
&lt;td&gt;Postgres requires typed record output for FROM clause functions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two most common pitfalls interviewers probe:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pitfall 1: forgetting &lt;code&gt;ORDER BY 1, 2&lt;/code&gt; in the first argument.&lt;/strong&gt; Crosstab does NOT re-sort internally; if the input is unordered, values can land in the wrong output cells silently. Always &lt;code&gt;ORDER BY row_key, category&lt;/code&gt; in the first argument.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pitfall 2: category-name type mismatch.&lt;/strong&gt; The second-argument &lt;code&gt;VALUES&lt;/code&gt; are matched to the &lt;code&gt;category&lt;/code&gt; column of the first argument by textual equality. If &lt;code&gt;metric_name&lt;/code&gt; is &lt;code&gt;text&lt;/code&gt; but you write &lt;code&gt;VALUES (1)&lt;/code&gt; (integer), the match fails and every cell is NULL. Cast both sides to &lt;code&gt;text&lt;/code&gt; if in doubt.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;date&lt;/th&gt;
&lt;th&gt;cpu_pct&lt;/th&gt;
&lt;th&gt;mem_mb&lt;/th&gt;
&lt;th&gt;disk_pct&lt;/th&gt;
&lt;th&gt;conn_count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2026-07-04&lt;/td&gt;
&lt;td&gt;55&lt;/td&gt;
&lt;td&gt;1200&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-07-05&lt;/td&gt;
&lt;td&gt;58&lt;/td&gt;
&lt;td&gt;1250&lt;/td&gt;
&lt;td&gt;61&lt;/td&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-07-06&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;1300&lt;/td&gt;
&lt;td&gt;63&lt;/td&gt;
&lt;td&gt;44&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Two-argument form pins column order&lt;/strong&gt;&lt;/strong&gt; — the &lt;code&gt;VALUES&lt;/code&gt; list is the source of truth for which categories appear and in what order. Categories in the input but not the list are dropped; categories in the list but not the input become NULL columns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;ORDER BY 1, 2 is non-negotiable&lt;/strong&gt;&lt;/strong&gt; — crosstab uses the row order to detect row-key boundaries. Skipping the &lt;code&gt;ORDER BY&lt;/code&gt; produces silent data corruption. Always sort by &lt;code&gt;(row_key, category)&lt;/code&gt; in the first argument.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;AS ct(…) declares record types&lt;/strong&gt;&lt;/strong&gt; — Postgres's &lt;code&gt;SETOF record&lt;/code&gt; return type requires the caller to name output columns and their types. The types must match the first argument's projection exactly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;NULL cells are the missing-value contract&lt;/strong&gt;&lt;/strong&gt; — if a &lt;code&gt;(date, metric_name)&lt;/code&gt; combination has no row, the cell is NULL. Wrap with &lt;code&gt;COALESCE(cpu_pct, 0)&lt;/code&gt; in the outer projection when the schema expects zeros.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — crosstab is a wrapper around the underlying &lt;code&gt;GROUP BY row_key&lt;/code&gt; scan plus a hash of categories. Cost is O(rows) plus O(categories) per row_key. Suitable for any reasonable metric-count workload; not a bottleneck.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — aggregation&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Aggregation and pivot problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/aggregation" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — case-when&lt;/span&gt;
&lt;strong&gt;CASE WHEN pivot fallback drills&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/case-when" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  3. UNPIVOT and dialect equivalents
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;sql unpivot&lt;/code&gt; is the mirror of PIVOT — SQL Server, Snowflake, BigQuery ship the keyword; Postgres and MySQL use UNION ALL or JSON helpers
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;UNPIVOT explodes named columns into &lt;code&gt;(name, value)&lt;/code&gt; pairs — SQL Server, Snowflake, and BigQuery ship a first-class &lt;code&gt;UNPIVOT&lt;/code&gt; keyword, while Postgres uses &lt;code&gt;UNION ALL&lt;/code&gt;, &lt;code&gt;unnest(ARRAY[...])&lt;/code&gt;, or &lt;code&gt;jsonb_each_text(row_to_json(t))&lt;/code&gt; to achieve the same shape, and MySQL falls back to &lt;code&gt;UNION ALL&lt;/code&gt; or &lt;code&gt;JSON_TABLE&lt;/code&gt;&lt;/strong&gt;. Once you say "wide-to-long has three named implementations and three portable fallbacks," the entire unpivot interview surface becomes a keyword lookup.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl058sq8cdwou8mqpcn2h.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl058sq8cdwou8mqpcn2h.jpeg" alt="Visual diagram of UNPIVOT dialect equivalents — a wide monthly-sales input on the left, a long month-value output on the right, and a 5-column matrix comparing SQL Server, Snowflake, BigQuery, Postgres UNION ALL, and Postgres jsonb_each_text; on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SQL Server UNPIVOT — the OG dialect.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Syntax: &lt;code&gt;SELECT keys, name_col, value_col FROM src UNPIVOT (value_col FOR name_col IN (col1, col2, col3)) AS p&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Requires an alias table (&lt;code&gt;AS p&lt;/code&gt;), same as PIVOT.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;value_col&lt;/code&gt; and &lt;code&gt;name_col&lt;/code&gt; are new column names you invent; &lt;code&gt;IN (…)&lt;/code&gt; lists the source columns to unpivot.&lt;/li&gt;
&lt;li&gt;All unpivoted columns must have the &lt;strong&gt;same data type&lt;/strong&gt; — SQL Server does not coerce. If your source has &lt;code&gt;int&lt;/code&gt; and &lt;code&gt;varchar&lt;/code&gt; mixed, cast to a common type in a subquery first.&lt;/li&gt;
&lt;li&gt;Drops &lt;code&gt;NULL&lt;/code&gt; cells by default; no &lt;code&gt;INCLUDE NULLS&lt;/code&gt; option — use &lt;code&gt;UNION ALL&lt;/code&gt; if you need every cell preserved.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Snowflake UNPIVOT.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Syntax: &lt;code&gt;SELECT * FROM t UNPIVOT (value_col FOR name_col IN (col1, col2, col3))&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Cleaner than SQL Server — no alias table required.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;INCLUDE NULLS&lt;/code&gt; option: &lt;code&gt;UNPIVOT INCLUDE NULLS (value_col FOR name_col IN (…))&lt;/code&gt; keeps NULL cells; default is &lt;code&gt;EXCLUDE NULLS&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Column aliases in the IN clause: &lt;code&gt;IN (jan AS 'January', feb AS 'February')&lt;/code&gt; renames the label values.&lt;/li&gt;
&lt;li&gt;Type coercion is stricter than PIVOT — the unpivoted columns must share a compatible type; mixing &lt;code&gt;NUMBER&lt;/code&gt; and &lt;code&gt;VARCHAR&lt;/code&gt; errors out.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;BigQuery UNPIVOT.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Syntax matches Snowflake almost line-for-line: &lt;code&gt;SELECT * FROM t UNPIVOT (value_col FOR name_col IN (col1, col2, col3))&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Also supports &lt;code&gt;INCLUDE NULLS&lt;/code&gt; and &lt;code&gt;EXCLUDE NULLS&lt;/code&gt; (default).&lt;/li&gt;
&lt;li&gt;The IN clause can also accept groups of columns for multi-value unpivots: &lt;code&gt;IN ((jan_sales, jan_cost) AS 'Jan', (feb_sales, feb_cost) AS 'Feb')&lt;/code&gt; — a Snowflake and BigQuery exclusive.&lt;/li&gt;
&lt;li&gt;BigQuery does NOT allow arbitrary subqueries in the IN list (unlike PIVOT's &lt;code&gt;IN&lt;/code&gt;); it must be a static column list.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Postgres — three portable equivalents.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;UNION ALL&lt;/code&gt;&lt;/strong&gt; — the classical form. One &lt;code&gt;SELECT&lt;/code&gt; per unpivoted column: &lt;code&gt;SELECT keys, 'jan' AS month, jan AS value FROM t UNION ALL SELECT keys, 'feb', feb FROM t …&lt;/code&gt;. Verbose for many columns but explicit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;unnest(ARRAY[...], ARRAY[...])&lt;/code&gt;&lt;/strong&gt; — the terse form using paired arrays. &lt;code&gt;SELECT keys, m.month, m.value FROM t, unnest(ARRAY['jan','feb','mar'], ARRAY[t.jan, t.feb, t.mar]) AS m(month, value)&lt;/code&gt;. One row per array element, per input row.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;jsonb_each_text(to_jsonb(t))&lt;/code&gt;&lt;/strong&gt; — the "unpivot every column" one-liner. Converts each row to a JSON object, then explodes each key-value pair. Great for wide tables where you don't want to type the column names.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;MySQL emulation.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No &lt;code&gt;UNPIVOT&lt;/code&gt; keyword. Options: &lt;code&gt;UNION ALL&lt;/code&gt; (works on every MySQL version), &lt;code&gt;JSON_TABLE&lt;/code&gt; (MySQL 8.0.4+, joins against a JSON array), stored procedure with dynamic SQL.&lt;/li&gt;
&lt;li&gt;The typical answer: &lt;code&gt;UNION ALL&lt;/code&gt; for 3-10 columns, &lt;code&gt;JSON_TABLE&lt;/code&gt; for wider tables.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When to pick which.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;≤ 5 columns to unpivot, portability required&lt;/strong&gt; — &lt;code&gt;UNION ALL&lt;/code&gt;. Every dialect. Verbose but bulletproof.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5-50 columns, single dialect (SQL Server / Snowflake / BigQuery)&lt;/strong&gt; — &lt;code&gt;UNPIVOT&lt;/code&gt; keyword. Boilerplate savings pay off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres, wide table (20+ columns), you want "unpivot everything"&lt;/strong&gt; — &lt;code&gt;jsonb_each_text(to_jsonb(t))&lt;/code&gt;. One line, no column list to maintain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BigQuery, multi-value unpivot (jan_sales + jan_cost together)&lt;/strong&gt; — grouped &lt;code&gt;IN&lt;/code&gt; clause. Snowflake and BigQuery exclusive.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on UNPIVOT.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"How do you UNPIVOT in Postgres?" — Postgres has no keyword; use &lt;code&gt;UNION ALL&lt;/code&gt;, &lt;code&gt;unnest(ARRAY[...], ARRAY[...])&lt;/code&gt;, or &lt;code&gt;jsonb_each_text(to_jsonb(t))&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;"What is the difference between INCLUDE NULLS and EXCLUDE NULLS?" — &lt;code&gt;EXCLUDE NULLS&lt;/code&gt; (default in Snowflake / BigQuery / SQL Server) drops rows where the unpivoted value is NULL; &lt;code&gt;INCLUDE NULLS&lt;/code&gt; keeps them.&lt;/li&gt;
&lt;li&gt;"How do you UNPIVOT in MySQL?" — &lt;code&gt;UNION ALL&lt;/code&gt;, &lt;code&gt;JSON_TABLE&lt;/code&gt;, or a stored procedure with dynamic SQL. No first-class keyword.&lt;/li&gt;
&lt;li&gt;"What is a multi-value unpivot?" — grouping multiple source columns to a single label: Snowflake / BigQuery &lt;code&gt;IN ((jan_sales, jan_cost) AS 'Jan', …)&lt;/code&gt;. Useful when your wide table has repeated column groups per period.&lt;/li&gt;
&lt;li&gt;"Why must UNPIVOT columns share a type?" — because the output &lt;code&gt;value_col&lt;/code&gt; has exactly one type; source columns are stacked into it, and SQL is strict about type unification.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — SQL Server UNPIVOT with mixed types
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A wide &lt;code&gt;metrics&lt;/code&gt; table has &lt;code&gt;(server_id, cpu_pct decimal, mem_mb bigint, disk_pct decimal)&lt;/code&gt;. Direct &lt;code&gt;UNPIVOT&lt;/code&gt; fails because &lt;code&gt;bigint&lt;/code&gt; and &lt;code&gt;decimal&lt;/code&gt; don't unify. The interviewer wants to see you cast to a common type in an inner subquery.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given a wide &lt;code&gt;metrics&lt;/code&gt; table with mixed numeric types, write a SQL Server &lt;code&gt;UNPIVOT&lt;/code&gt; that produces &lt;code&gt;(server_id, metric_name, value)&lt;/code&gt; in long form. Handle the type-mismatch error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;server_id&lt;/th&gt;
&lt;th&gt;cpu_pct&lt;/th&gt;
&lt;th&gt;mem_mb&lt;/th&gt;
&lt;th&gt;disk_pct&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;s1&lt;/td&gt;
&lt;td&gt;55.0&lt;/td&gt;
&lt;td&gt;8192&lt;/td&gt;
&lt;td&gt;60.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;s2&lt;/td&gt;
&lt;td&gt;62.0&lt;/td&gt;
&lt;td&gt;16384&lt;/td&gt;
&lt;td&gt;45.2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- BROKEN — mixed types cause "operand type clash" error&lt;/span&gt;
&lt;span class="c1"&gt;-- SELECT server_id, metric_name, value&lt;/span&gt;
&lt;span class="c1"&gt;-- FROM metrics&lt;/span&gt;
&lt;span class="c1"&gt;-- UNPIVOT (value FOR metric_name IN (cpu_pct, mem_mb, disk_pct)) AS p;&lt;/span&gt;

&lt;span class="c1"&gt;-- Fixed — cast all source columns to a common type in inner subquery&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;server_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;metric_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;server_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cpu_pct&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;cpu_pct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mem_mb&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;mem_mb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;disk_pct&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;disk_pct&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;metrics&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;
&lt;span class="n"&gt;UNPIVOT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="n"&gt;metric_name&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cpu_pct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mem_mb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;disk_pct&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;UNPIVOT&lt;/code&gt; requires every column in the &lt;code&gt;IN&lt;/code&gt; list to have the same data type — the resulting &lt;code&gt;value&lt;/code&gt; column can only be one type.&lt;/li&gt;
&lt;li&gt;Without the inner cast, SQL Server throws &lt;code&gt;The type of column "cpu_pct" conflicts with the type of other columns specified in the UNPIVOT list&lt;/code&gt;. The error message is unusually clear here.&lt;/li&gt;
&lt;li&gt;The inner subquery casts all three source columns to &lt;code&gt;decimal(18,4)&lt;/code&gt; — a common type that holds both the integer &lt;code&gt;mem_mb&lt;/code&gt; and the decimal &lt;code&gt;cpu_pct&lt;/code&gt; / &lt;code&gt;disk_pct&lt;/code&gt; values without loss.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;UNPIVOT (value FOR metric_name IN (…))&lt;/code&gt; reads: "emit one row per input row per named column; put the column name as text in &lt;code&gt;metric_name&lt;/code&gt; and the column value in &lt;code&gt;value&lt;/code&gt;."&lt;/li&gt;
&lt;li&gt;NULL cells are dropped silently (no &lt;code&gt;INCLUDE NULLS&lt;/code&gt; in SQL Server). If a server has NULL &lt;code&gt;disk_pct&lt;/code&gt;, that row is missing from the output — you have to add &lt;code&gt;UNION ALL&lt;/code&gt; for the missing rows if you need them.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;server_id&lt;/th&gt;
&lt;th&gt;metric_name&lt;/th&gt;
&lt;th&gt;value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;s1&lt;/td&gt;
&lt;td&gt;cpu_pct&lt;/td&gt;
&lt;td&gt;55.0000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;s1&lt;/td&gt;
&lt;td&gt;mem_mb&lt;/td&gt;
&lt;td&gt;8192.0000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;s1&lt;/td&gt;
&lt;td&gt;disk_pct&lt;/td&gt;
&lt;td&gt;60.5000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;s2&lt;/td&gt;
&lt;td&gt;cpu_pct&lt;/td&gt;
&lt;td&gt;62.0000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;s2&lt;/td&gt;
&lt;td&gt;mem_mb&lt;/td&gt;
&lt;td&gt;16384.0000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;s2&lt;/td&gt;
&lt;td&gt;disk_pct&lt;/td&gt;
&lt;td&gt;45.2000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Whenever the columns you want to &lt;code&gt;UNPIVOT&lt;/code&gt; don't share a type, cast them to a common one in an inner subquery. &lt;code&gt;decimal(18,4)&lt;/code&gt; is a safe superset for most numeric mixes; &lt;code&gt;varchar(4000)&lt;/code&gt; or &lt;code&gt;nvarchar(max)&lt;/code&gt; for text mixes.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — Snowflake UNPIVOT with INCLUDE NULLS
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A wide &lt;code&gt;attendance&lt;/code&gt; table records student attendance across 5 days. Some cells are NULL because the student wasn't enrolled that day. The interviewer wants a long-form view where NULL rows are preserved (so the count of enrolled students matches).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given a wide &lt;code&gt;attendance&lt;/code&gt; table with &lt;code&gt;(student_id, mon, tue, wed, thu, fri)&lt;/code&gt;, write a Snowflake &lt;code&gt;UNPIVOT&lt;/code&gt; that preserves NULL cells. Explain when to use &lt;code&gt;INCLUDE NULLS&lt;/code&gt; vs &lt;code&gt;EXCLUDE NULLS&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;student_id&lt;/th&gt;
&lt;th&gt;mon&lt;/th&gt;
&lt;th&gt;tue&lt;/th&gt;
&lt;th&gt;wed&lt;/th&gt;
&lt;th&gt;thu&lt;/th&gt;
&lt;th&gt;fri&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;s1&lt;/td&gt;
&lt;td&gt;present&lt;/td&gt;
&lt;td&gt;present&lt;/td&gt;
&lt;td&gt;absent&lt;/td&gt;
&lt;td&gt;present&lt;/td&gt;
&lt;td&gt;present&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;s2&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;present&lt;/td&gt;
&lt;td&gt;present&lt;/td&gt;
&lt;td&gt;present&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Default EXCLUDE NULLS — drops NULL cells&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;student_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;attendance&lt;/span&gt;
&lt;span class="n"&gt;UNPIVOT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;thu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fri&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;-- INCLUDE NULLS — preserves every cell, useful for enrolment analysis&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;student_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;attendance&lt;/span&gt;
&lt;span class="n"&gt;UNPIVOT&lt;/span&gt; &lt;span class="n"&gt;INCLUDE&lt;/span&gt; &lt;span class="n"&gt;NULLS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;thu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fri&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Default &lt;code&gt;UNPIVOT&lt;/code&gt; behavior in Snowflake is &lt;code&gt;EXCLUDE NULLS&lt;/code&gt;. Cells where the source column is &lt;code&gt;NULL&lt;/code&gt; do not produce a row in the output.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;INCLUDE NULLS&lt;/code&gt; reverses that — every &lt;code&gt;(student_id, day)&lt;/code&gt; pair produces a row, with &lt;code&gt;status&lt;/code&gt; = &lt;code&gt;NULL&lt;/code&gt; if the source cell was &lt;code&gt;NULL&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;When to use &lt;code&gt;EXCLUDE NULLS&lt;/code&gt;: analytics workloads where NULL means "not applicable" and shouldn't inflate row counts. Attendance-percentage calculations often use this to exclude non-enrolled days.&lt;/li&gt;
&lt;li&gt;When to use &lt;code&gt;INCLUDE NULLS&lt;/code&gt;: audit workloads where you need to know every source cell was considered. Enrolment-vs-attendance reports need this to differentiate "not enrolled" from "absent."&lt;/li&gt;
&lt;li&gt;BigQuery has identical &lt;code&gt;INCLUDE NULLS&lt;/code&gt; / &lt;code&gt;EXCLUDE NULLS&lt;/code&gt; semantics. SQL Server does not — it always excludes; use &lt;code&gt;UNION ALL&lt;/code&gt; for the include case.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output (INCLUDE NULLS variant).&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;student_id&lt;/th&gt;
&lt;th&gt;day&lt;/th&gt;
&lt;th&gt;status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;s1&lt;/td&gt;
&lt;td&gt;mon&lt;/td&gt;
&lt;td&gt;present&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;s1&lt;/td&gt;
&lt;td&gt;tue&lt;/td&gt;
&lt;td&gt;present&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;s1&lt;/td&gt;
&lt;td&gt;wed&lt;/td&gt;
&lt;td&gt;absent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;s1&lt;/td&gt;
&lt;td&gt;thu&lt;/td&gt;
&lt;td&gt;present&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;s1&lt;/td&gt;
&lt;td&gt;fri&lt;/td&gt;
&lt;td&gt;present&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;s2&lt;/td&gt;
&lt;td&gt;mon&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;s2&lt;/td&gt;
&lt;td&gt;tue&lt;/td&gt;
&lt;td&gt;present&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;s2&lt;/td&gt;
&lt;td&gt;wed&lt;/td&gt;
&lt;td&gt;present&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;s2&lt;/td&gt;
&lt;td&gt;thu&lt;/td&gt;
&lt;td&gt;present&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;s2&lt;/td&gt;
&lt;td&gt;fri&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Name the NULL-handling explicitly in every Snowflake &lt;code&gt;UNPIVOT&lt;/code&gt; — either &lt;code&gt;INCLUDE NULLS&lt;/code&gt; or &lt;code&gt;EXCLUDE NULLS&lt;/code&gt;. Relying on the default is a bug magnet when a downstream query counts rows and misses the NULL cases.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — Postgres jsonb_each_text for wide-table unpivot
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A wide &lt;code&gt;daily_stats&lt;/code&gt; table has 30 metric columns. Typing them into a &lt;code&gt;UNION ALL&lt;/code&gt; would be 30 branches. Postgres has a one-liner: convert each row to a JSON object with &lt;code&gt;to_jsonb(t)&lt;/code&gt;, then explode each key-value pair with &lt;code&gt;jsonb_each_text&lt;/code&gt;. Zero column-name maintenance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given a wide &lt;code&gt;daily_stats&lt;/code&gt; table with columns &lt;code&gt;(date, m1, m2, m3, m4, m5, …)&lt;/code&gt;, write a Postgres query that produces long-form &lt;code&gt;(date, metric_name, value)&lt;/code&gt; without listing every column. Explain the mechanics of &lt;code&gt;to_jsonb + jsonb_each_text&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;date&lt;/th&gt;
&lt;th&gt;m1&lt;/th&gt;
&lt;th&gt;m2&lt;/th&gt;
&lt;th&gt;m3&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2026-07-01&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-07-02&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;35&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;key&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;metric_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;value_text&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;daily_stats&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="k"&gt;LATERAL&lt;/span&gt; &lt;span class="n"&gt;jsonb_each_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to_jsonb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'date'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;kv&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;to_jsonb(t)&lt;/code&gt; converts the entire row &lt;code&gt;t&lt;/code&gt; into a JSON object where each column name is a key and each value is the JSON representation of the cell. For row &lt;code&gt;('2026-07-01', 10, 20, 30)&lt;/code&gt;, it produces &lt;code&gt;{"date": "2026-07-01", "m1": 10, "m2": 20, "m3": 30}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;- 'date'&lt;/code&gt; operator removes the &lt;code&gt;date&lt;/code&gt; key from the JSON object so the date doesn't end up as a metric row. Standard set-minus for &lt;code&gt;jsonb&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;jsonb_each_text(json_obj)&lt;/code&gt; returns a set of &lt;code&gt;(key text, value text)&lt;/code&gt; rows, one per key-value pair in the object. For the row above, it emits &lt;code&gt;(m1, 10)&lt;/code&gt;, &lt;code&gt;(m2, 20)&lt;/code&gt;, &lt;code&gt;(m3, 30)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LATERAL&lt;/code&gt; allows the function to reference the outer row &lt;code&gt;t&lt;/code&gt; — required whenever a table-returning function depends on the current row. Without &lt;code&gt;LATERAL&lt;/code&gt;, Postgres wouldn't allow &lt;code&gt;t&lt;/code&gt; inside &lt;code&gt;jsonb_each_text(to_jsonb(t))&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The output is long-form &lt;code&gt;(date, metric_name, value_text)&lt;/code&gt; for every metric column in the row. Adding a new metric column to &lt;code&gt;daily_stats&lt;/code&gt; requires zero changes to this query — that's the whole reason to use this pattern.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;date&lt;/th&gt;
&lt;th&gt;metric_name&lt;/th&gt;
&lt;th&gt;value_text&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2026-07-01&lt;/td&gt;
&lt;td&gt;m1&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-07-01&lt;/td&gt;
&lt;td&gt;m2&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-07-01&lt;/td&gt;
&lt;td&gt;m3&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-07-02&lt;/td&gt;
&lt;td&gt;m1&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-07-02&lt;/td&gt;
&lt;td&gt;m2&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-07-02&lt;/td&gt;
&lt;td&gt;m3&lt;/td&gt;
&lt;td&gt;35&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For any Postgres unpivot with more than 5 columns, reach for &lt;code&gt;jsonb_each_text(to_jsonb(t) - 'excluded_col')&lt;/code&gt;. It's declarative, requires zero column-name maintenance, and survives schema changes. Cast the &lt;code&gt;value&lt;/code&gt; back to the target type (&lt;code&gt;value::int&lt;/code&gt;, &lt;code&gt;value::numeric&lt;/code&gt;) in an outer projection if you need numeric aggregation.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — Postgres unnest for a fixed column set
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; When you know the exact columns to unpivot but don't want the &lt;code&gt;UNION ALL&lt;/code&gt; boilerplate, Postgres's paired-array &lt;code&gt;unnest&lt;/code&gt; is the terse form. It stays declarative and produces a compact SQL that reads well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given a &lt;code&gt;weekly_sales&lt;/code&gt; table with &lt;code&gt;(product, mon, tue, wed, thu, fri)&lt;/code&gt;, write a Postgres query using &lt;code&gt;unnest(ARRAY[...], ARRAY[...])&lt;/code&gt; that produces &lt;code&gt;(product, day, sales)&lt;/code&gt; in long form.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;product&lt;/th&gt;
&lt;th&gt;mon&lt;/th&gt;
&lt;th&gt;tue&lt;/th&gt;
&lt;th&gt;wed&lt;/th&gt;
&lt;th&gt;thu&lt;/th&gt;
&lt;th&gt;fri&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sales&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;weekly_sales&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="k"&gt;LATERAL&lt;/span&gt; &lt;span class="k"&gt;unnest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
       &lt;span class="n"&gt;ARRAY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'mon'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'tue'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'thu'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'fri'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
       &lt;span class="n"&gt;ARRAY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;thu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fri&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
     &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;unnest(ARRAY[...], ARRAY[...])&lt;/code&gt; is a two-argument variant that pairs elements: element 1 of the first array pairs with element 1 of the second, and so on. The arrays MUST have the same length.&lt;/li&gt;
&lt;li&gt;The first array holds the day labels (&lt;code&gt;'mon'&lt;/code&gt;, &lt;code&gt;'tue'&lt;/code&gt;, …). The second holds the values from the current row (&lt;code&gt;t.mon&lt;/code&gt;, &lt;code&gt;t.tue&lt;/code&gt;, …).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AS d (day, sales)&lt;/code&gt; names the two output columns from the paired unnest.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LATERAL&lt;/code&gt; is required because the second array references &lt;code&gt;t&lt;/code&gt; — the outer row. Postgres executes &lt;code&gt;unnest&lt;/code&gt; once per outer row, producing 5 rows per input row (one per day label).&lt;/li&gt;
&lt;li&gt;Compared to the &lt;code&gt;UNION ALL&lt;/code&gt; variant, this is one SELECT statement instead of five. Compared to &lt;code&gt;jsonb_each_text&lt;/code&gt;, it's more explicit about the label set — great when you want to name and order the categories.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;product&lt;/th&gt;
&lt;th&gt;day&lt;/th&gt;
&lt;th&gt;sales&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;mon&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;tue&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;wed&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;thu&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;fri&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;mon&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;tue&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;wed&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;thu&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;fri&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Use &lt;code&gt;unnest(ARRAY[...], ARRAY[...])&lt;/code&gt; when the label list is small and fixed and you want a single-statement Postgres unpivot. Use &lt;code&gt;jsonb_each_text(to_jsonb(t))&lt;/code&gt; when the column set is large or evolving. Both are more idiomatic than &lt;code&gt;UNION ALL&lt;/code&gt; for anything beyond 3-4 columns.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on portable UNPIVOT
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "You're writing a data-pipeline library that needs to work on Postgres, MySQL, Snowflake, and SQL Server. Write a wide-to-long reshape that runs identically on all four. What syntax do you pick and why?"&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using UNION ALL as the portable canonical form
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Portable wide-to-long — works on every dialect&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'jan'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jan&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales_wide&lt;/span&gt;
&lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'feb'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;feb&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales_wide&lt;/span&gt;
&lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'mar'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mar&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales_wide&lt;/span&gt;
&lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'apr'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;apr&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales_wide&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- For "include NULLs" everywhere, don't filter — WHERE sales IS NOT NULL&lt;/span&gt;
&lt;span class="c1"&gt;-- if you want the drop-nulls behavior of default UNPIVOT.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;What runs&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;First SELECT emits (product, 'jan', jan) for every row&lt;/td&gt;
&lt;td&gt;N rows for Jan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;UNION ALL appends without dedup&lt;/td&gt;
&lt;td&gt;N rows preserved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Second SELECT emits (product, 'feb', feb)&lt;/td&gt;
&lt;td&gt;N rows for Feb&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Repeat for every month&lt;/td&gt;
&lt;td&gt;4×N rows total&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Optionally WHERE sales IS NOT NULL on outer&lt;/td&gt;
&lt;td&gt;Emulates EXCLUDE NULLS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pattern trades N branches of typing for total portability. For 4 columns it's a 4-branch query; for 30 columns it's a 30-branch query — at that point, you build the query in Python or dbt instead of writing it by hand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;product&lt;/th&gt;
&lt;th&gt;month&lt;/th&gt;
&lt;th&gt;sales&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;jan&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;feb&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;mar&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;apr&lt;/td&gt;
&lt;td&gt;175&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;jan&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;feb&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;mar&lt;/td&gt;
&lt;td&gt;110&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;apr&lt;/td&gt;
&lt;td&gt;95&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;UNION ALL is the ANSI SQL-92 fallback&lt;/strong&gt;&lt;/strong&gt; — every dialect since the 90s supports it. If your query needs to run on Postgres AND MySQL AND SQL Server AND Snowflake, &lt;code&gt;UNION ALL&lt;/code&gt; is the only mechanism guaranteed to work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;One SELECT per unpivoted column&lt;/strong&gt;&lt;/strong&gt; — the syntax is verbose but explicit. Every column is named, every label is named, every projection is explicit. Code reviewers can spot missing columns immediately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;WHERE clauses on outer SELECT emulate NULL-handling&lt;/strong&gt;&lt;/strong&gt; — wrap the whole &lt;code&gt;UNION ALL&lt;/code&gt; in a subquery and add &lt;code&gt;WHERE sales IS NOT NULL&lt;/code&gt; to emulate &lt;code&gt;EXCLUDE NULLS&lt;/code&gt;. Omit the filter for &lt;code&gt;INCLUDE NULLS&lt;/code&gt; behavior.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Code-generation friendly&lt;/strong&gt;&lt;/strong&gt; — if you have 30 columns, a 10-line Python loop or a dbt Jinja &lt;code&gt;for&lt;/code&gt; block writes the 30 branches. The generated SQL is boring and readable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — every branch scans the table once. Cost is O(N × rows) where N is the branch count. For a 30-column unpivot on a 100M row table, that's 3B row-touches — at that scale, switch to warehouse-native &lt;code&gt;UNPIVOT&lt;/code&gt; or materialise the long form once.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — unpivoting&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;UNION ALL unpivot problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/unpivoting" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — case-expression&lt;/span&gt;
&lt;strong&gt;CASE expression reshape drills&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/case-expression" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  4. Dynamic pivots — unknown columns at compile time
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;dynamic pivot sql&lt;/code&gt; — Snowflake ANY ORDER BY, PREPARE / EXECUTE elsewhere, dbt macros for the analytics layer
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;a dynamic pivot happens when the label set is unknown at query-authoring time (new product categories, unknown month range, custom user attributes) — Snowflake handles it declaratively with &lt;code&gt;IN (ANY ORDER BY col)&lt;/code&gt;, everyone else builds a SQL string from a metadata query and runs it with &lt;code&gt;PREPARE&lt;/code&gt; / &lt;code&gt;EXECUTE&lt;/code&gt; or &lt;code&gt;EXECUTE IMMEDIATE&lt;/code&gt;, and analytics engineers side-step the whole class with dbt macros or Python code generators&lt;/strong&gt;. Once you say "know the labels? static PIVOT. Don't know them? dynamic answer differs by dialect," the entire dynamic-pivot interview surface collapses to a four-way pattern selection.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6iscjqvotc4gmknzxnqn.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6iscjqvotc4gmknzxnqn.jpeg" alt="Visual flow of dynamic pivots — a metadata query feeding into a string-build step producing a column list, then an EXECUTE IMMEDIATE step running the built SQL; a Snowflake ANY ORDER BY card on the right showing the flagship declarative form; a dbt macro card at the bottom; on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The dynamic-pivot problem statement.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Static pivot: &lt;code&gt;PIVOT(SUM(sales) FOR month IN ('Jan', 'Feb', 'Mar'))&lt;/code&gt;. The labels are hard-coded.&lt;/li&gt;
&lt;li&gt;Dynamic pivot: &lt;code&gt;PIVOT(SUM(sales) FOR month IN (?))&lt;/code&gt; where &lt;code&gt;?&lt;/code&gt; is filled from &lt;code&gt;SELECT DISTINCT month FROM sales&lt;/code&gt;. A new month next week? The pivot picks it up automatically.&lt;/li&gt;
&lt;li&gt;The general recipe (non-Snowflake): (1) query the metadata for distinct labels, (2) build the pivot SQL string with the labels interpolated, (3) execute the string.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Snowflake &lt;code&gt;ANY ORDER BY&lt;/code&gt; — the flagship dynamic form.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Syntax: &lt;code&gt;SELECT * FROM t PIVOT(SUM(sales) FOR month IN (ANY ORDER BY month))&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ANY&lt;/code&gt; tells Snowflake: "discover the label set at query time from the input."&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ORDER BY month&lt;/code&gt; pins the column order in the output. Without it, order is undefined.&lt;/li&gt;
&lt;li&gt;Works for any label type (string, number, date). Output columns are named exactly as the label values.&lt;/li&gt;
&lt;li&gt;No &lt;code&gt;EXECUTE IMMEDIATE&lt;/code&gt; required — this is Snowflake's declarative dynamic-pivot answer, GA in 2023.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;SQL Server dynamic pivot with &lt;code&gt;PREPARE&lt;/code&gt; / &lt;code&gt;EXECUTE&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build a comma-separated bracketed-label string with &lt;code&gt;STRING_AGG(QUOTENAME(month), ',')&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Concatenate into a full &lt;code&gt;PIVOT&lt;/code&gt; SQL string.&lt;/li&gt;
&lt;li&gt;Run with &lt;code&gt;EXEC(@sql)&lt;/code&gt; or &lt;code&gt;sp_executesql @sql&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The classic three-step: metadata → string build → execute.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;BigQuery &lt;code&gt;EXECUTE IMMEDIATE&lt;/code&gt; for dynamic pivot.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Same pattern as SQL Server: build the label string, concatenate into a pivot statement, run with &lt;code&gt;EXECUTE IMMEDIATE @sql USING @params&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;BigQuery has no &lt;code&gt;ANY&lt;/code&gt; form; you must build the string.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;@params&lt;/code&gt; USING clause supports parameter binding for safety.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;dbt macro pattern for analytics engineers.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;At &lt;code&gt;dbt compile&lt;/code&gt; time, run a &lt;code&gt;run_query('SELECT DISTINCT month FROM {{ ref("sales") }}')&lt;/code&gt; inside a Jinja block.&lt;/li&gt;
&lt;li&gt;Loop over the returned values to emit &lt;code&gt;SUM(CASE WHEN month = '{{ col }}' THEN sales END) AS {{ col }}&lt;/code&gt; for each label.&lt;/li&gt;
&lt;li&gt;The compiled SQL is static from the warehouse's perspective — no &lt;code&gt;EXECUTE IMMEDIATE&lt;/code&gt; needed.&lt;/li&gt;
&lt;li&gt;This is the analytics-engineer canonical answer because it works on every dialect.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Python-generated SQL.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When the analytics-engineer answer doesn't fit (out-of-cycle report, one-off query), a small Python script queries the labels, builds the SQL, and runs it against the warehouse.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pandas.DataFrame.pivot_table&lt;/code&gt; does the pivot client-side after &lt;code&gt;pd.read_sql&lt;/code&gt;; useful when the pivot is downstream of a small warehouse query.&lt;/li&gt;
&lt;li&gt;SQLAlchemy's Query API lets you build the pivot expression programmatically for stronger typing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on dynamic pivots.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"How do you handle a pivot when the column set is unknown at compile time?" — Snowflake: &lt;code&gt;ANY ORDER BY&lt;/code&gt;; SQL Server: &lt;code&gt;PREPARE + EXECUTE&lt;/code&gt;; BigQuery: &lt;code&gt;EXECUTE IMMEDIATE&lt;/code&gt;; dbt: Jinja &lt;code&gt;{% for %}&lt;/code&gt; macro; else: build the SQL in Python.&lt;/li&gt;
&lt;li&gt;"What is &lt;code&gt;ANY ORDER BY&lt;/code&gt; in Snowflake?" — the declarative dynamic-pivot form; auto-discovers labels, produces one column per distinct label.&lt;/li&gt;
&lt;li&gt;"How do you avoid SQL injection in dynamic pivots?" — use &lt;code&gt;QUOTENAME&lt;/code&gt; in SQL Server, &lt;code&gt;format('%I')&lt;/code&gt; in Postgres, &lt;code&gt;EXECUTE IMMEDIATE … USING @params&lt;/code&gt; in BigQuery. Never string-concatenate untrusted input.&lt;/li&gt;
&lt;li&gt;"When would you use a dbt macro instead of dynamic SQL?" — every time you can. Compiled SQL is static, cacheable, and testable; dynamic SQL is opaque to the query planner.&lt;/li&gt;
&lt;li&gt;"What's the failure mode of dynamic pivots?" — a new label the day the report runs causes a schema drift downstream (dashboards break, dbt tests fail, BI models need updates). Always document the discovered label set.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — Snowflake ANY ORDER BY for a growing product catalog
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A &lt;code&gt;sales&lt;/code&gt; table grows new product categories weekly. Static &lt;code&gt;PIVOT&lt;/code&gt; would need updating every week; &lt;code&gt;ANY ORDER BY&lt;/code&gt; discovers the label set at query time. This is the pattern to reach for on Snowflake — it's declarative, fast, and requires zero dynamic SQL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given a &lt;code&gt;sales(month, product, amount)&lt;/code&gt; table where new products are added over time, write a Snowflake query that produces a monthly matrix with one column per product, discovered at query time. Show the &lt;code&gt;ANY ORDER BY&lt;/code&gt; syntax and its trade-offs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;month&lt;/th&gt;
&lt;th&gt;product&lt;/th&gt;
&lt;th&gt;amount&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Jan&lt;/td&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jan&lt;/td&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jan&lt;/td&gt;
&lt;td&gt;Gizmo&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feb&lt;/td&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feb&lt;/td&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feb&lt;/td&gt;
&lt;td&gt;Gizmo&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feb&lt;/td&gt;
&lt;td&gt;Doohickey&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Snowflake dynamic pivot — ANY ORDER BY, no dynamic SQL&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;PIVOT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANY&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The subquery &lt;code&gt;SELECT month, product, amount FROM sales&lt;/code&gt; establishes the input shape. &lt;code&gt;month&lt;/code&gt; becomes the implicit grouping key.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PIVOT (SUM(amount) FOR product IN (ANY ORDER BY product))&lt;/code&gt; reads: "pivot on the &lt;code&gt;product&lt;/code&gt; column; discover every distinct product value; sort the output columns alphabetically by product."&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ANY&lt;/code&gt; triggers Snowflake's dynamic-label discovery. At execution time, Snowflake scans the input to find distinct product values, then produces one output column per value.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ORDER BY product&lt;/code&gt; pins the output column order to alphabetical by product name. Without it, the order is undefined and could change between runs — that's a downstream-breaking bug waiting to happen.&lt;/li&gt;
&lt;li&gt;When a new product (e.g., &lt;code&gt;'Doohickey'&lt;/code&gt;) appears in &lt;code&gt;Feb&lt;/code&gt;, it automatically gets a column in the output. Old months without that product show &lt;code&gt;NULL&lt;/code&gt; in the new column. Downstream consumers must handle schema drift explicitly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;month&lt;/th&gt;
&lt;th&gt;Doohickey&lt;/th&gt;
&lt;th&gt;Gadget&lt;/th&gt;
&lt;th&gt;Gizmo&lt;/th&gt;
&lt;th&gt;Widget&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Feb&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jan&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; On Snowflake, always use &lt;code&gt;ANY ORDER BY &amp;lt;col&amp;gt;&lt;/code&gt; for dynamic pivots. Skip &lt;code&gt;EXECUTE IMMEDIATE&lt;/code&gt; entirely — the declarative form is faster, testable, and can't be SQL-injected. Warn downstream consumers about schema drift when new labels can appear.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — SQL Server dynamic pivot with PREPARE / EXECUTE
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; SQL Server has no &lt;code&gt;ANY&lt;/code&gt; form. The canonical pattern is three-step: query distinct labels, build a comma-separated bracketed list with &lt;code&gt;STRING_AGG(QUOTENAME(col), ',')&lt;/code&gt;, and execute the concatenated pivot string.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given a SQL Server &lt;code&gt;sales(month, product, amount)&lt;/code&gt; table, write a dynamic pivot that discovers products at query time. Show the metadata query, string build, and execution steps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;month&lt;/th&gt;
&lt;th&gt;product&lt;/th&gt;
&lt;th&gt;amount&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Jan&lt;/td&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jan&lt;/td&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feb&lt;/td&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feb&lt;/td&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feb&lt;/td&gt;
&lt;td&gt;Gizmo&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Step 1 — build the bracketed label list from a metadata query&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;cols&lt;/span&gt; &lt;span class="n"&gt;NVARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;cols&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;STRING_AGG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;QUOTENAME&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;','&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- @cols now = '[Gadget],[Gizmo],[Widget]'&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 2 — build the full pivot SQL string&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;sql&lt;/span&gt; &lt;span class="n"&gt;NVARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'SELECT month, '&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;cols&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'
    FROM (SELECT month, product, amount FROM sales) src
    PIVOT (SUM(amount) FOR product IN ('&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;cols&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;')) AS p;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 3 — execute the dynamic SQL&lt;/span&gt;
&lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="n"&gt;sp_executesql&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;STRING_AGG(QUOTENAME(product), ',')&lt;/code&gt; produces the comma-separated bracketed label list. &lt;code&gt;QUOTENAME&lt;/code&gt; wraps each label in brackets AND handles any special characters (spaces, reserved words) — critical for SQL injection safety.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;SELECT DISTINCT product FROM sales&lt;/code&gt; inside the subquery is the metadata query; it produces one row per unique product. &lt;code&gt;STRING_AGG&lt;/code&gt; collapses those rows into a single string.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;@sql&lt;/code&gt; variable is built by string concatenation: the outer projection lists the bracketed labels, and the &lt;code&gt;PIVOT&lt;/code&gt; clause repeats them in the &lt;code&gt;IN&lt;/code&gt; list. Note that &lt;code&gt;@cols&lt;/code&gt; is used twice.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sp_executesql @sql&lt;/code&gt; is preferred over &lt;code&gt;EXEC(@sql)&lt;/code&gt; because it caches the query plan and supports parameter binding for further safety. For this specific case, the plan cache benefit is small because each run's &lt;code&gt;@sql&lt;/code&gt; string is different by design.&lt;/li&gt;
&lt;li&gt;To wrap this in a stored procedure for reuse, parameterise the aggregate function and the source table name. Use &lt;code&gt;sp_executesql @sql, N'@table NVARCHAR(128)', @table = 'sales'&lt;/code&gt; for parameter binding.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;month&lt;/th&gt;
&lt;th&gt;Gadget&lt;/th&gt;
&lt;th&gt;Gizmo&lt;/th&gt;
&lt;th&gt;Widget&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Jan&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feb&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Always use &lt;code&gt;QUOTENAME&lt;/code&gt; in &lt;code&gt;STRING_AGG&lt;/code&gt; to build the label list — it's the SQL-injection-safe way to interpolate identifiers. Never string-concatenate raw user input into dynamic pivot SQL; a hostile category name like &lt;code&gt;'Widget]) SELECT * FROM users; --'&lt;/code&gt; would break the query without proper escaping.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — BigQuery EXECUTE IMMEDIATE dynamic pivot
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; BigQuery has no &lt;code&gt;ANY&lt;/code&gt; form and requires the string-build approach. &lt;code&gt;EXECUTE IMMEDIATE&lt;/code&gt; is the mechanism; the label discovery uses &lt;code&gt;STRING_AGG&lt;/code&gt; inside a scripting block.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given a BigQuery &lt;code&gt;sales(month, product, amount)&lt;/code&gt; table, write a dynamic pivot that discovers products at query time. Show the scripting block that builds and executes the pivot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;month&lt;/th&gt;
&lt;th&gt;product&lt;/th&gt;
&lt;th&gt;amount&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Jan&lt;/td&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feb&lt;/td&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feb&lt;/td&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- BigQuery scripting block — dynamic pivot with EXECUTE IMMEDIATE&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="n"&gt;cols&lt;/span&gt; &lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="k"&gt;sql&lt;/span&gt; &lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 1 — build the label list&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;cols&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;STRING_AGG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;CONCAT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;"'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;"'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;`project.dataset.sales`&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- cols now = "'Gadget','Widget'"&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 2 — build the pivot statement&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FORMAT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;"&lt;/span&gt;&lt;span class="se"&gt;""&lt;/span&gt;&lt;span class="nv"&gt;
  SELECT * FROM (
    SELECT month, product, amount FROM `project.dataset.sales`
  )
  PIVOT (SUM(amount) FOR product IN (%s))
&lt;/span&gt;&lt;span class="se"&gt;""&lt;/span&gt;&lt;span class="nv"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 3 — run it&lt;/span&gt;
&lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="k"&gt;IMMEDIATE&lt;/span&gt; &lt;span class="k"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;DECLARE&lt;/code&gt; introduces script-level variables. &lt;code&gt;cols&lt;/code&gt; will hold the label string; &lt;code&gt;sql&lt;/code&gt; will hold the full pivot statement.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;STRING_AGG(DISTINCT CONCAT("'", product, "'") ORDER BY product)&lt;/code&gt; builds a quoted-string list &lt;code&gt;'Gadget','Widget'&lt;/code&gt;. The &lt;code&gt;DISTINCT&lt;/code&gt; deduplicates; the &lt;code&gt;ORDER BY&lt;/code&gt; fixes column order.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FORMAT("... %s ...", cols)&lt;/code&gt; is BigQuery's printf-style string interpolation. Cleaner than string concatenation with &lt;code&gt;||&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;EXECUTE IMMEDIATE sql&lt;/code&gt; runs the built SQL. The result set is emitted as if you'd written the pivot statically.&lt;/li&gt;
&lt;li&gt;For parameterised queries with user input, use &lt;code&gt;EXECUTE IMMEDIATE sql USING @param1, @param2&lt;/code&gt; — BigQuery's binding syntax that avoids injection.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;month&lt;/th&gt;
&lt;th&gt;Gadget&lt;/th&gt;
&lt;th&gt;Widget&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Jan&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feb&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; On BigQuery, wrap dynamic pivots in a scripting block with &lt;code&gt;DECLARE&lt;/code&gt; + &lt;code&gt;EXECUTE IMMEDIATE&lt;/code&gt;. For labels sourced from user input, always use the &lt;code&gt;USING @param&lt;/code&gt; binding form — string concatenation is the classic SQL-injection vector.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — dbt macro pivot for analytics engineers
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; In an analytics-engineering workflow, dbt compiles Jinja to SQL at build time. A pivot macro can query the label list at compile time, then emit static &lt;code&gt;SUM(CASE WHEN …)&lt;/code&gt; expressions for each label. The compiled SQL is portable across every dialect that supports basic &lt;code&gt;SUM&lt;/code&gt; and &lt;code&gt;CASE WHEN&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write a dbt macro &lt;code&gt;pivot(source_model, column, pivot_column, agg)&lt;/code&gt; that emits a static pivot at compile time. Show its use for a monthly-sales report.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt; The &lt;code&gt;sales&lt;/code&gt; model has &lt;code&gt;(month, product, amount)&lt;/code&gt;. The macro will produce one column per distinct product.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- macros/pivot.sql&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;macro&lt;/span&gt; &lt;span class="n"&gt;pivot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pivot_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;agg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'sum'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;labels_query&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="k"&gt;distinct&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;pivot_column&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;
    &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source_model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
    &lt;span class="k"&gt;order&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;endset&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;labels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;run_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;labels_query&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="k"&gt;values&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;labels&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;agg&lt;/span&gt; &lt;span class="p"&gt;}}(&lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;pivot_column&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'{{ label }}'&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;column&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'_'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;lower&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%-&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;last&lt;/span&gt; &lt;span class="o"&gt;-%&lt;/span&gt;&lt;span class="p"&gt;},{&lt;/span&gt;&lt;span class="o"&gt;%-&lt;/span&gt; &lt;span class="n"&gt;endif&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;endfor&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;endmacro&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- models/monthly_pivot.sql&lt;/span&gt;
&lt;span class="k"&gt;select&lt;/span&gt;
  &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;pivot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sales'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'amount'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'product'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sales'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="k"&gt;group&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;
&lt;span class="k"&gt;order&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;pivot&lt;/code&gt; macro takes four arguments: the source model name, the value column, the label column, and the aggregate function (defaulting to &lt;code&gt;sum&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;At compile time, &lt;code&gt;run_query()&lt;/code&gt; executes the &lt;code&gt;labels_query&lt;/code&gt; against the warehouse to fetch every distinct product. This is a one-shot metadata call.&lt;/li&gt;
&lt;li&gt;The Jinja &lt;code&gt;{% for label in labels %}&lt;/code&gt; loop emits one &lt;code&gt;SUM(CASE WHEN product = 'X' THEN amount END) AS x&lt;/code&gt; expression per label. &lt;code&gt;loop.last&lt;/code&gt; controls the trailing comma.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;replace(' ', '_') | lower&lt;/code&gt; filter normalises label names into snake-case identifiers safe for output column names.&lt;/li&gt;
&lt;li&gt;The compiled result is plain static SQL — every warehouse can execute it. No dynamic SQL, no &lt;code&gt;EXECUTE IMMEDIATE&lt;/code&gt;, no injection risk. The macro handles the dynamic label discovery at compile time and emits static output.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output (compiled SQL for two labels — Widget, Gadget).&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt;
  &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Widget'&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;widget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Gadget'&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;gadget&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;
&lt;span class="k"&gt;group&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;
&lt;span class="k"&gt;order&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For any dbt project that needs a dynamic pivot, write a pivot macro once and reuse it. The compiled SQL is static, testable with dbt tests, and portable — you can migrate the same model from Snowflake to BigQuery without changing the macro call. Analytics engineers should reach for compile-time code generation over runtime &lt;code&gt;EXECUTE IMMEDIATE&lt;/code&gt; whenever possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on dynamic pivot design
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "You're building a reporting system where the pivot column set can grow monthly. How do you architect the dynamic pivot, and how do you protect downstream consumers from schema drift?"&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using compile-time codegen + explicit label contract
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Dynamic pivot architecture — compile-time codegen + schema-drift discipline
===========================================================================

Design decision 1 — where does the pivot happen?
  - dbt macro (compile time)  → BEST, works everywhere
  - Snowflake ANY ORDER BY    → good on Snowflake, still drifts
  - EXECUTE IMMEDIATE         → last resort; hard to test

Design decision 2 — how do downstream consumers handle drift?
  - Contract table: every new label must be added to
    `label_registry(label, first_seen_date, retired_date)`
    with a code review before the pivot picks it up.
  - Materialise the pivot output; downstream reads a stable schema.
  - Emit an event on new-label detection so the BI team is notified.

Design decision 3 — what happens on label removal?
  - Retired labels stay in the output as zero-filled columns until
    the retention window ends, then are dropped.
  - Never silently drop a label — dashboards will break.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Query metadata for distinct labels&lt;/td&gt;
&lt;td&gt;Live label list&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Join against &lt;code&gt;label_registry&lt;/code&gt; — keep only registered labels&lt;/td&gt;
&lt;td&gt;Approved label list&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Compile pivot with approved list&lt;/td&gt;
&lt;td&gt;Static SQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Materialise pivot output to a stable table&lt;/td&gt;
&lt;td&gt;Stable schema&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Nightly job: detect new labels, emit event to registry team&lt;/td&gt;
&lt;td&gt;Drift alert&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;On approval, next compile picks up the new label&lt;/td&gt;
&lt;td&gt;Controlled rollout&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The architecture turns "silent schema drift" into "explicit review workflow." Downstream dashboards and BI models see a stable schema; new labels go through a change-management step before they hit production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Metadata&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;label_registry&lt;/code&gt; table&lt;/td&gt;
&lt;td&gt;Contract of approved labels&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compile&lt;/td&gt;
&lt;td&gt;dbt pivot macro&lt;/td&gt;
&lt;td&gt;Emits static SQL, respects registry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warehouse&lt;/td&gt;
&lt;td&gt;Materialised pivot table&lt;/td&gt;
&lt;td&gt;Stable output for BI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ops&lt;/td&gt;
&lt;td&gt;Nightly drift detector&lt;/td&gt;
&lt;td&gt;Alerts on unregistered labels&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Compile-time codegen&lt;/strong&gt;&lt;/strong&gt; — dbt macros run once per model build, emit static SQL, and are testable with dbt tests. Runtime &lt;code&gt;EXECUTE IMMEDIATE&lt;/code&gt; is opaque to the query planner and the test framework.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Label registry as a contract&lt;/strong&gt;&lt;/strong&gt; — a &lt;code&gt;label_registry&lt;/code&gt; table converts "new label appeared in source" from a silent event into an explicit workflow. Engineers see it, review it, and approve it before it hits production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Materialisation stabilises downstream&lt;/strong&gt;&lt;/strong&gt; — writing the pivot output to a table (or dbt incremental model) means BI consumers read a fixed schema. Their dashboards don't break when the pivot picks up a new label mid-day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Explicit retirement&lt;/strong&gt;&lt;/strong&gt; — retired labels stay as zero-filled columns for a retention window. Deleting a column on the day it stops appearing in source data breaks every downstream that references it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — the compile-time metadata query is one round trip per dbt run. The registry table is negligible. The materialisation is one full-scan-plus-write per refresh. Total cost is comparable to a static pivot; the value is in the drift-control workflow, not throughput.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — aggregation&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Dynamic pivot aggregation drills&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/aggregation" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — grouping sets&lt;/span&gt;
&lt;strong&gt;Grouping sets and multi-level reports&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/grouping-sets" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  5. Conditional-aggregation fallback
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;SUM(CASE WHEN …)&lt;/code&gt; is the universal portable pivot; ANSI &lt;code&gt;FILTER (WHERE …)&lt;/code&gt; is the cleaner form where supported
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;&lt;code&gt;SUM(CASE WHEN col = 'X' THEN val END) AS x&lt;/code&gt; is the pivot recipe that works in every SQL dialect ever shipped, and the ANSI &lt;code&gt;FILTER (WHERE …)&lt;/code&gt; clause is the cleaner Postgres / SQLite / DuckDB form that plans to the exact same execution — so conditional aggregation is the senior go-to whenever portability, mixed aggregates, or tiny column sets matter more than syntactic sugar&lt;/strong&gt;. Once you internalise "PIVOT is sugar over &lt;code&gt;SUM(CASE WHEN …)&lt;/code&gt;," every dialect gap collapses to a template you fill in.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fom1rn1fniq6k0y6g6cys.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fom1rn1fniq6k0y6g6cys.jpeg" alt="Visual diagram of the conditional aggregation fallback — a SUM(CASE WHEN …) card on the left and a FILTER (WHERE …) card on the right, with a bottom card explaining when conditional aggregation wins over PIVOT (mixed aggregates, tiny column sets, portability, debuggability); on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;SUM(CASE WHEN …)&lt;/code&gt; — the universal recipe.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Template: &lt;code&gt;SUM(CASE WHEN pivot_col = 'label' THEN value_col END) AS label&lt;/code&gt;. Repeat for each label.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;THEN value_col&lt;/code&gt; (no &lt;code&gt;ELSE&lt;/code&gt;) makes non-matching rows contribute &lt;code&gt;NULL&lt;/code&gt;, which &lt;code&gt;SUM&lt;/code&gt; ignores. Do NOT write &lt;code&gt;ELSE 0&lt;/code&gt; — that inflates counts and, more subtly, breaks &lt;code&gt;AVG&lt;/code&gt; if you swap the aggregate.&lt;/li&gt;
&lt;li&gt;The outer query needs a &lt;code&gt;GROUP BY&lt;/code&gt; on any column NOT in the &lt;code&gt;SUM&lt;/code&gt; expression (the row keys). Any column that appears bare in &lt;code&gt;SELECT&lt;/code&gt; must be in &lt;code&gt;GROUP BY&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Works in every SQL dialect ever shipped: Postgres, MySQL, MariaDB, SQL Server, Snowflake, BigQuery, Oracle, DB2, SQLite, DuckDB, ClickHouse. Portability is unmatched.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;FILTER (WHERE …)&lt;/code&gt; — the ANSI cleaner form.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Syntax: &lt;code&gt;SUM(value_col) FILTER (WHERE pivot_col = 'label') AS label&lt;/code&gt;. The &lt;code&gt;FILTER&lt;/code&gt; clause post-filters rows for that specific aggregate.&lt;/li&gt;
&lt;li&gt;ANSI-standard since SQL:2003.&lt;/li&gt;
&lt;li&gt;Supported in: Postgres 9.4+, SQLite 3.30+, DuckDB, Firebird.&lt;/li&gt;
&lt;li&gt;NOT supported in: Snowflake, BigQuery, SQL Server, MySQL, MariaDB, Oracle.&lt;/li&gt;
&lt;li&gt;The query plan is identical to &lt;code&gt;SUM(CASE WHEN …)&lt;/code&gt; — this is pure syntactic sugar for readability.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When conditional aggregation wins over &lt;code&gt;PIVOT&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mixed aggregates in one row.&lt;/strong&gt; &lt;code&gt;SUM(CASE WHEN month = 'Jan' THEN amount END) AS jan_sum, COUNT(CASE WHEN month = 'Jan' THEN 1 END) AS jan_count, MAX(CASE WHEN month = 'Jan' THEN amount END) AS jan_max&lt;/code&gt;. &lt;code&gt;PIVOT&lt;/code&gt; gives one aggregate per output column set; &lt;code&gt;CASE WHEN&lt;/code&gt; lets you mix freely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tiny column sets.&lt;/strong&gt; For 3–5 pivoted columns, the boilerplate saved by &lt;code&gt;PIVOT&lt;/code&gt; is small. The debuggability of a plain &lt;code&gt;SELECT&lt;/code&gt; + &lt;code&gt;GROUP BY&lt;/code&gt; often wins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dialect portability.&lt;/strong&gt; One query, four warehouses. Only conditional aggregation runs identically everywhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debuggability.&lt;/strong&gt; Every runner explains the plan the same way. &lt;code&gt;PIVOT&lt;/code&gt; in some warehouses hides the plan behind a syntactic sugar layer that's harder to inspect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Predicate integration.&lt;/strong&gt; Every label branch is a plain SQL expression, so you can add arbitrary conditions: &lt;code&gt;SUM(CASE WHEN month = 'Jan' AND region = 'US' THEN amount END) AS jan_us&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When &lt;code&gt;PIVOT&lt;/code&gt; wins over conditional aggregation.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Many output columns (10+).&lt;/strong&gt; &lt;code&gt;PIVOT(SUM(amount) FOR month IN ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'))&lt;/code&gt; reads as one line. The equivalent &lt;code&gt;SUM(CASE WHEN …)&lt;/code&gt; is 12 lines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code brevity in a Snowflake / BigQuery / SQL Server single-dialect codebase.&lt;/strong&gt; If you're never going to port the query, use the sugar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic column ergonomics.&lt;/strong&gt; Snowflake &lt;code&gt;ANY ORDER BY&lt;/code&gt; is far cleaner than any hand-coded &lt;code&gt;CASE WHEN&lt;/code&gt; generator for unknown label sets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analyst readability.&lt;/strong&gt; BI teams often read &lt;code&gt;PIVOT&lt;/code&gt; more easily than a stack of &lt;code&gt;CASE WHEN&lt;/code&gt; expressions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Performance parity.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every warehouse compiles &lt;code&gt;PIVOT&lt;/code&gt; down to the same plan as &lt;code&gt;SUM(CASE WHEN …)&lt;/code&gt;: a &lt;code&gt;GROUP BY&lt;/code&gt; scan with per-column expressions in the projection.&lt;/li&gt;
&lt;li&gt;Empirically confirmed on Postgres, Snowflake, BigQuery, SQL Server — same plan, same runtime.&lt;/li&gt;
&lt;li&gt;The one exception: some warehouses may vectorise &lt;code&gt;PIVOT&lt;/code&gt; slightly better on wide pivots because they can pre-compute the label bitmap. This is a micro-optimisation; ignore it unless you profile and see a difference.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on conditional aggregation.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"How do you pivot in a dialect with no PIVOT keyword?" — &lt;code&gt;SUM(CASE WHEN pivot_col = 'label' THEN val END) AS label&lt;/code&gt;, repeated per label, with &lt;code&gt;GROUP BY&lt;/code&gt; on the row-key columns.&lt;/li&gt;
&lt;li&gt;"What is &lt;code&gt;FILTER (WHERE …)&lt;/code&gt; and where does it work?" — the ANSI-standard alternative to &lt;code&gt;CASE WHEN&lt;/code&gt; inside an aggregate; supported in Postgres, SQLite, DuckDB.&lt;/li&gt;
&lt;li&gt;"Why not write &lt;code&gt;ELSE 0&lt;/code&gt; in the CASE?" — because non-matching rows should contribute NULL (which SUM ignores). &lt;code&gt;ELSE 0&lt;/code&gt; inflates COUNT and breaks AVG if you swap the aggregate.&lt;/li&gt;
&lt;li&gt;"Same plan as PIVOT?" — yes. &lt;code&gt;PIVOT&lt;/code&gt; is syntactic sugar over &lt;code&gt;SUM(CASE WHEN …)&lt;/code&gt; under the hood in every modern warehouse.&lt;/li&gt;
&lt;li&gt;"How do you do mixed aggregates (SUM + COUNT)?" — conditional aggregation lets you: &lt;code&gt;SUM(CASE WHEN …) AS s, COUNT(CASE WHEN …) AS c&lt;/code&gt;. &lt;code&gt;PIVOT&lt;/code&gt; cannot.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — SUM(CASE WHEN …) monthly pivot
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The canonical portable pivot: given a long &lt;code&gt;(product, month, sales)&lt;/code&gt; table, emit one row per product with columns for each month. Written with &lt;code&gt;SUM(CASE WHEN …)&lt;/code&gt;, this query runs identically on every dialect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write a monthly-sales pivot in &lt;code&gt;SUM(CASE WHEN …)&lt;/code&gt; form that produces one row per product with columns for January, February, and March. Explain why &lt;code&gt;ELSE 0&lt;/code&gt; should NOT be used.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;product&lt;/th&gt;
&lt;th&gt;month&lt;/th&gt;
&lt;th&gt;sales&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;Jan&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;Feb&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;Mar&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;Jan&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;Mar&lt;/td&gt;
&lt;td&gt;110&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Jan'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;jan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Feb'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;feb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Mar'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;mar&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;CASE WHEN month = 'Jan' THEN sales END&lt;/code&gt; returns &lt;code&gt;sales&lt;/code&gt; when the row's month is 'Jan' and &lt;code&gt;NULL&lt;/code&gt; otherwise. &lt;code&gt;SUM&lt;/code&gt; ignores NULLs by definition, so the total is the sum of only the 'Jan' rows.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GROUP BY product&lt;/code&gt; is required because &lt;code&gt;product&lt;/code&gt; appears bare in the projection. Every column not inside an aggregate must be in &lt;code&gt;GROUP BY&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The three &lt;code&gt;SUM(CASE WHEN …)&lt;/code&gt; expressions each pick a subset of the input and sum them. Same table scan for all three — the planner reads the table once and evaluates the three expressions per row.&lt;/li&gt;
&lt;li&gt;Non-matching cells come out as &lt;code&gt;NULL&lt;/code&gt; (Gadget in February has no row). To display &lt;code&gt;0&lt;/code&gt; instead, wrap with &lt;code&gt;COALESCE(SUM(CASE WHEN …), 0)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Do NOT write &lt;code&gt;ELSE 0&lt;/code&gt; inside the CASE. &lt;code&gt;SUM(CASE WHEN month = 'Jan' THEN sales ELSE 0 END)&lt;/code&gt; would work for SUM but breaks two ways: (a) &lt;code&gt;COUNT(CASE WHEN month = 'Jan' THEN sales ELSE 0 END)&lt;/code&gt; counts every row (including 0s), inflating the count; (b) &lt;code&gt;AVG(CASE WHEN month = 'Jan' THEN sales ELSE 0 END)&lt;/code&gt; averages the zeros in, producing the wrong mean.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;product&lt;/th&gt;
&lt;th&gt;jan&lt;/th&gt;
&lt;th&gt;feb&lt;/th&gt;
&lt;th&gt;mar&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;110&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Always write &lt;code&gt;CASE WHEN pred THEN val END&lt;/code&gt; (with no &lt;code&gt;ELSE&lt;/code&gt;). Wrap the whole aggregate with &lt;code&gt;COALESCE(…, 0)&lt;/code&gt; if you need zero-instead-of-null in the output. This one habit prevents 90% of accidental aggregation bugs.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — mixed aggregates in one row
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A retail report needs monthly &lt;code&gt;sum&lt;/code&gt;, &lt;code&gt;count&lt;/code&gt;, &lt;code&gt;max&lt;/code&gt;, and &lt;code&gt;avg&lt;/code&gt; per product, all in one row. &lt;code&gt;PIVOT&lt;/code&gt; cannot express this — it gives you one aggregate per output column set. Conditional aggregation nails it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given the same &lt;code&gt;sales&lt;/code&gt; table, produce one row per product with columns &lt;code&gt;jan_sum&lt;/code&gt;, &lt;code&gt;jan_count&lt;/code&gt;, &lt;code&gt;jan_max&lt;/code&gt;, &lt;code&gt;feb_sum&lt;/code&gt;, &lt;code&gt;feb_count&lt;/code&gt;, &lt;code&gt;feb_max&lt;/code&gt;. Show that conditional aggregation makes this trivial and &lt;code&gt;PIVOT&lt;/code&gt; cannot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;product&lt;/th&gt;
&lt;th&gt;month&lt;/th&gt;
&lt;th&gt;sales&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;Jan&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;Jan&lt;/td&gt;
&lt;td&gt;70&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;Feb&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;Jan&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;Feb&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;Feb&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

  &lt;span class="c1"&gt;-- January metrics&lt;/span&gt;
  &lt;span class="k"&gt;SUM&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Jan'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;jan_sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Jan'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;     &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;jan_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;MAX&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Jan'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;jan_max&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

  &lt;span class="c1"&gt;-- February metrics&lt;/span&gt;
  &lt;span class="k"&gt;SUM&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Feb'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;feb_sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Feb'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;     &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;feb_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;MAX&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Feb'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;feb_max&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Every metric per month is its own &lt;code&gt;CASE WHEN&lt;/code&gt; inside its own aggregate. &lt;code&gt;SUM&lt;/code&gt; sums the &lt;code&gt;sales&lt;/code&gt;, &lt;code&gt;COUNT&lt;/code&gt; counts the 1s, &lt;code&gt;MAX&lt;/code&gt; takes the largest value.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;COUNT(CASE WHEN month = 'Jan' THEN 1 END)&lt;/code&gt; counts rows where &lt;code&gt;month = 'Jan'&lt;/code&gt;. The &lt;code&gt;1&lt;/code&gt; is arbitrary — any non-NULL value works. This is equivalent to &lt;code&gt;COUNT(*)&lt;/code&gt; filtered to Jan rows.&lt;/li&gt;
&lt;li&gt;Widget in Jan has two rows (30, 70) → &lt;code&gt;sum = 100&lt;/code&gt;, &lt;code&gt;count = 2&lt;/code&gt;, &lt;code&gt;max = 70&lt;/code&gt;. The query computes all three in one pass.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PIVOT(SUM(sales) FOR month IN ('Jan', 'Feb'))&lt;/code&gt; gives only the sums — you can't ask for sum, count, and max in one &lt;code&gt;PIVOT&lt;/code&gt;. To get all three, you'd need three separate &lt;code&gt;PIVOT&lt;/code&gt; subqueries joined together, or you'd have to concede and use &lt;code&gt;SUM(CASE WHEN …)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;This is the killer case for conditional aggregation. Every senior data engineer knows: when you need mixed aggregates, &lt;code&gt;PIVOT&lt;/code&gt; is out and &lt;code&gt;CASE WHEN&lt;/code&gt; is in.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;product&lt;/th&gt;
&lt;th&gt;jan_sum&lt;/th&gt;
&lt;th&gt;jan_count&lt;/th&gt;
&lt;th&gt;jan_max&lt;/th&gt;
&lt;th&gt;feb_sum&lt;/th&gt;
&lt;th&gt;feb_count&lt;/th&gt;
&lt;th&gt;feb_max&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;70&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Any time an interviewer says "and also count / max / avg per month," reach for conditional aggregation. &lt;code&gt;PIVOT&lt;/code&gt; is a one-aggregate tool; &lt;code&gt;CASE WHEN&lt;/code&gt; is the multi-aggregate tool. Learning to recognise the mixed-aggregate cue in the interview question is a senior signal.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — FILTER (WHERE …) on Postgres
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Postgres, SQLite, and DuckDB all support the ANSI &lt;code&gt;FILTER (WHERE …)&lt;/code&gt; clause. It reads more like natural language — "SUM of sales filtered to January" — and plans identically to the CASE WHEN form. Postgres teams should default to it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Rewrite the previous mixed-aggregate query using &lt;code&gt;FILTER (WHERE …)&lt;/code&gt;. Show the plan is identical to the CASE WHEN version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt; Same as the previous example.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

  &lt;span class="c1"&gt;-- January metrics with FILTER&lt;/span&gt;
  &lt;span class="k"&gt;SUM&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sales&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Jan'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;jan_sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Jan'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;jan_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;MAX&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sales&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Jan'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;jan_max&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

  &lt;span class="c1"&gt;-- February metrics with FILTER&lt;/span&gt;
  &lt;span class="k"&gt;SUM&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sales&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Feb'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;feb_sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Feb'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;feb_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;MAX&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sales&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Feb'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;feb_max&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;SUM(sales) FILTER (WHERE month = 'Jan')&lt;/code&gt; reads: "sum the &lt;code&gt;sales&lt;/code&gt; column, but only for rows where &lt;code&gt;month = 'Jan'&lt;/code&gt;." The &lt;code&gt;FILTER&lt;/code&gt; clause post-filters rows for that specific aggregate.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;COUNT(*) FILTER (WHERE month = 'Jan')&lt;/code&gt; counts rows matching the filter. Cleaner than &lt;code&gt;COUNT(CASE WHEN month = 'Jan' THEN 1 END)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The Postgres query planner compiles this to the same executor plan as the CASE WHEN version — you can confirm with &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt;. Both show a single &lt;code&gt;GroupAggregate&lt;/code&gt; node with N aggregate expressions.&lt;/li&gt;
&lt;li&gt;Postgres also supports &lt;code&gt;FILTER&lt;/code&gt; on window functions: &lt;code&gt;SUM(sales) FILTER (WHERE month = 'Jan') OVER (PARTITION BY region)&lt;/code&gt;. This is a Postgres exclusive as of 2026.&lt;/li&gt;
&lt;li&gt;Snowflake, BigQuery, SQL Server, and MySQL do NOT support &lt;code&gt;FILTER&lt;/code&gt;. Trying to use it there produces a syntax error — always confirm your target dialect before adopting &lt;code&gt;FILTER&lt;/code&gt; in a codebase.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt; Identical to the CASE WHEN version — same rows, same columns, same values.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; On any Postgres, SQLite, or DuckDB project, adopt &lt;code&gt;FILTER (WHERE …)&lt;/code&gt; as the house style for conditional aggregates. It's more readable, plans identically, and follows the SQL standard. Just remember it doesn't port to the four biggest cloud warehouses.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — conditional aggregation for a portable multi-dialect pipeline
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A data-pipeline library needs a monthly pivot that runs identically on Postgres, Snowflake, BigQuery, and SQL Server. &lt;code&gt;SUM(CASE WHEN …)&lt;/code&gt; is the only mechanism that satisfies portability. Show the recipe with &lt;code&gt;COALESCE&lt;/code&gt; for missing-cell defaults.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write a portable monthly pivot for &lt;code&gt;(product, month, sales)&lt;/code&gt; that runs on Postgres, Snowflake, BigQuery, and SQL Server without modification. Handle missing cells with &lt;code&gt;COALESCE(0)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;product&lt;/th&gt;
&lt;th&gt;month&lt;/th&gt;
&lt;th&gt;sales&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;Jan&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;Feb&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;Jan&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Portable across Postgres, Snowflake, BigQuery, SQL Server, MySQL&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Jan'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;jan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Feb'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;feb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Mar'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;mar&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;SUM(CASE WHEN …)&lt;/code&gt; core is the portable primitive. Every dialect ships &lt;code&gt;SUM&lt;/code&gt;, &lt;code&gt;CASE WHEN&lt;/code&gt;, &lt;code&gt;GROUP BY&lt;/code&gt;, and &lt;code&gt;ORDER BY&lt;/code&gt; — no dialect-specific syntax needed.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;COALESCE(SUM(…), 0)&lt;/code&gt; replaces the &lt;code&gt;NULL&lt;/code&gt; cells (product-month combinations with no rows) with &lt;code&gt;0&lt;/code&gt;. &lt;code&gt;COALESCE&lt;/code&gt; is ANSI-standard and works on every dialect. (Postgres, MySQL, SQL Server all also support &lt;code&gt;IFNULL&lt;/code&gt; / &lt;code&gt;ISNULL&lt;/code&gt; — but &lt;code&gt;COALESCE&lt;/code&gt; is the portable choice.)&lt;/li&gt;
&lt;li&gt;The query has no dialect-specific features: no &lt;code&gt;PIVOT&lt;/code&gt;, no &lt;code&gt;crosstab&lt;/code&gt;, no &lt;code&gt;FILTER&lt;/code&gt;, no bracketed identifiers. Runs identically on every warehouse the pipeline touches.&lt;/li&gt;
&lt;li&gt;To add a fourth month, add a fourth &lt;code&gt;COALESCE(SUM(CASE WHEN …), 0)&lt;/code&gt; line. To swap the aggregate to &lt;code&gt;MAX&lt;/code&gt;, change all three &lt;code&gt;SUM&lt;/code&gt;s to &lt;code&gt;MAX&lt;/code&gt;. No structural rework.&lt;/li&gt;
&lt;li&gt;If the library maintainer needs to adapt this to a dialect that has &lt;code&gt;PIVOT&lt;/code&gt; for readability, the swap is mechanical. But the reverse — &lt;code&gt;PIVOT&lt;/code&gt; code that needs to become portable — requires a full rewrite.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;product&lt;/th&gt;
&lt;th&gt;jan&lt;/th&gt;
&lt;th&gt;feb&lt;/th&gt;
&lt;th&gt;mar&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gadget&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Widget&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For any pipeline that runs across multiple warehouses, write conditional aggregation as the canonical form. Add native &lt;code&gt;PIVOT&lt;/code&gt; variants only when a specific dialect's readability wins a code-review argument. The portable form is the "single source of truth"; native forms are optimisations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on picking the right shape
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "You're reviewing a pull request that uses Snowflake &lt;code&gt;PIVOT&lt;/code&gt; for a monthly-sales report. When would you push back and ask for &lt;code&gt;SUM(CASE WHEN …)&lt;/code&gt; instead, and what would the review comment say?"&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using the "portability + mixed-aggregate + column-count" trilemma
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Review checklist — PIVOT vs SUM(CASE WHEN …)

1. Portability
   - Will this query ever run on a non-Snowflake warehouse?
     yes → push back on PIVOT; require SUM(CASE WHEN …)
     no  → PIVOT OK for readability

2. Mixed aggregates
   - Does the report emit SUM + COUNT + MAX + AVG per label?
     yes → PIVOT cannot express this; require SUM(CASE WHEN …)
     no  → PIVOT works

3. Column count
   - Fewer than 6 output columns?
     yes → SUM(CASE WHEN …) is roughly the same length; either OK
     no  → PIVOT wins on brevity

4. Dynamic labels?
   - Unknown label set at compile time?
     yes → Snowflake ANY ORDER BY, or dbt macro emitting CASE WHEN
     no  → static form of either

5. Dialect-specific features (COALESCE, DEFAULT ON NULL)?
   - Need zero-fill on missing cells?
     yes → wrap SUM(CASE WHEN …) with COALESCE; PIVOT needs
           dialect-specific option (Snowflake DEFAULT ON NULL,
           SQL Server outer COALESCE)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;PR under review&lt;/th&gt;
&lt;th&gt;Q1 portability&lt;/th&gt;
&lt;th&gt;Q2 mixed aggs&lt;/th&gt;
&lt;th&gt;Q3 col count&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sales report, Snowflake-only, 12-month PIVOT&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;Approve PIVOT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sales report, Postgres + Snowflake&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Push back → SUM CASE WHEN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ops report, SUM + COUNT + MAX per month&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;Push back → SUM CASE WHEN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dashboard, unknown categories&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;dynamic&lt;/td&gt;
&lt;td&gt;Approve ANY ORDER BY&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Small ad-hoc, 3 columns, one-off&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Either OK&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The review checklist turns "PIVOT or not?" from a taste question into a mechanical decision. Portability and mixed-aggregate concerns force &lt;code&gt;CASE WHEN&lt;/code&gt;; column count is a tie-breaker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;th&gt;Reasoning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Approve PIVOT&lt;/td&gt;
&lt;td&gt;Single dialect, single aggregate, many columns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Push back → CASE WHEN&lt;/td&gt;
&lt;td&gt;Portability required OR mixed aggregates OR tiny column count&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Approve ANY ORDER BY&lt;/td&gt;
&lt;td&gt;Snowflake dynamic labels, single aggregate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Push back → dbt macro&lt;/td&gt;
&lt;td&gt;Dynamic labels across multiple warehouses&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Trilemma of PIVOT trade-offs&lt;/strong&gt;&lt;/strong&gt; — portability, mixed aggregates, and column count are the three axes that decide whether &lt;code&gt;PIVOT&lt;/code&gt; fits. Fix any two axes and the third is determined.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;PIVOT is syntactic sugar&lt;/strong&gt;&lt;/strong&gt; — it compiles to &lt;code&gt;SUM(CASE WHEN …)&lt;/code&gt; in every warehouse. So the choice between them is a readability trade-off, not a performance one. Reviews should focus on maintainability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Mixed aggregates force CASE WHEN&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;PIVOT&lt;/code&gt; cannot mix SUM + COUNT + MAX in one output row. Any report that needs those alongside each other must use conditional aggregation. This is the killer question in the review checklist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Portability is a hard constraint&lt;/strong&gt;&lt;/strong&gt; — one query on multiple warehouses cannot use &lt;code&gt;PIVOT&lt;/code&gt; (Postgres has none, MySQL has none). The moment cross-dialect matters, &lt;code&gt;PIVOT&lt;/code&gt; is out and &lt;code&gt;SUM(CASE WHEN …)&lt;/code&gt; is in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — every strategy is O(rows) with one table scan. Column count multiplies the projection cost linearly. Wide pivots (30+ columns) may benefit from &lt;code&gt;PIVOT&lt;/code&gt;'s vectorisation on some warehouses; this is a micro-optimisation, not a design driver.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — conditional aggregation&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Conditional aggregation problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/conditional-aggregation" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;


&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — SQL&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL interview problem library&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;





&lt;h2&gt;
  
  
  Cheat sheet — pivot and unpivot recipes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Monthly-sales pivot in Snowflake.&lt;/strong&gt; &lt;code&gt;SELECT * FROM (SELECT product, month, sales FROM t) PIVOT(SUM(sales) FOR month IN ('Jan', 'Feb', 'Mar'))&lt;/code&gt;. Inline subquery + quoted labels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monthly-sales pivot in BigQuery.&lt;/strong&gt; Same shape as Snowflake but the subquery form is mandatory. &lt;code&gt;PIVOT(SUM(sales) FOR month IN ('Jan', 'Feb', 'Mar'))&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monthly-sales pivot in SQL Server.&lt;/strong&gt; &lt;code&gt;PIVOT(SUM(sales) FOR month IN ([Jan], [Feb], [Mar])) AS p&lt;/code&gt;. Bracketed labels + mandatory alias table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monthly-sales pivot in Postgres.&lt;/strong&gt; &lt;code&gt;SELECT * FROM crosstab($$…$$, $$VALUES ('Jan'), ('Feb'), ('Mar')$$) AS ct(product text, jan int, feb int, mar int)&lt;/code&gt;. Two-argument form with declared output columns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Portable monthly pivot.&lt;/strong&gt; &lt;code&gt;SELECT product, SUM(CASE WHEN month = 'Jan' THEN sales END) AS jan, SUM(CASE WHEN month = 'Feb' THEN sales END) AS feb FROM t GROUP BY product&lt;/code&gt;. Runs on every dialect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UNPIVOT in Snowflake / BigQuery.&lt;/strong&gt; &lt;code&gt;SELECT * FROM t UNPIVOT (sales FOR month IN (jan, feb, mar))&lt;/code&gt;. Add &lt;code&gt;INCLUDE NULLS&lt;/code&gt; when you need to preserve NULL cells.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UNPIVOT in SQL Server.&lt;/strong&gt; &lt;code&gt;SELECT keys, month, sales FROM src UNPIVOT (sales FOR month IN (jan, feb, mar)) AS p&lt;/code&gt;. All source columns must share a type — cast in inner subquery if mixed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UNPIVOT in Postgres via UNION ALL.&lt;/strong&gt; &lt;code&gt;SELECT product, 'jan' AS month, jan AS sales FROM t UNION ALL SELECT product, 'feb', feb FROM t …&lt;/code&gt;. Verbose but explicit; portable to MySQL too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UNPIVOT in Postgres via jsonb_each_text.&lt;/strong&gt; &lt;code&gt;SELECT keys, key AS name, value FROM t, LATERAL jsonb_each_text(to_jsonb(t) - 'keys')&lt;/code&gt;. Zero column-name maintenance; great for wide tables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UNPIVOT in Postgres via unnest.&lt;/strong&gt; &lt;code&gt;SELECT product, d.month, d.sales FROM t, LATERAL unnest(ARRAY['jan','feb','mar'], ARRAY[t.jan, t.feb, t.mar]) AS d(month, sales)&lt;/code&gt;. Fixed label set, single statement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic pivot in Snowflake.&lt;/strong&gt; &lt;code&gt;PIVOT(SUM(sales) FOR product IN (ANY ORDER BY product))&lt;/code&gt;. Native, declarative, no dynamic SQL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic pivot in SQL Server.&lt;/strong&gt; Build a &lt;code&gt;STRING_AGG(QUOTENAME(product), ',')&lt;/code&gt; label list, concatenate into the pivot SQL, run with &lt;code&gt;sp_executesql @sql&lt;/code&gt;. Always use &lt;code&gt;QUOTENAME&lt;/code&gt; for injection safety.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic pivot in BigQuery.&lt;/strong&gt; Build the label string with &lt;code&gt;STRING_AGG&lt;/code&gt; inside a scripting block, run with &lt;code&gt;EXECUTE IMMEDIATE sql USING @params&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic pivot in dbt.&lt;/strong&gt; Macro that calls &lt;code&gt;run_query()&lt;/code&gt; at compile time, loops labels in Jinja, emits static &lt;code&gt;SUM(CASE WHEN col = 'X' THEN val END) AS x&lt;/code&gt;. Portable output, testable, no runtime dynamic SQL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conditional aggregation template.&lt;/strong&gt; &lt;code&gt;SUM(CASE WHEN pivot_col = 'label' THEN val END) AS label&lt;/code&gt;. No &lt;code&gt;ELSE 0&lt;/code&gt; — non-matching rows should contribute &lt;code&gt;NULL&lt;/code&gt;. Wrap with &lt;code&gt;COALESCE(…, 0)&lt;/code&gt; for zero-fill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;FILTER (WHERE …)&lt;/code&gt; form.&lt;/strong&gt; &lt;code&gt;SUM(sales) FILTER (WHERE month = 'Jan') AS jan&lt;/code&gt;. Postgres, SQLite, DuckDB only. Plans identically to &lt;code&gt;CASE WHEN&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing-cell defaults.&lt;/strong&gt; Snowflake: &lt;code&gt;PIVOT(…) DEFAULT ON NULL (0)&lt;/code&gt;. SQL Server: &lt;code&gt;COALESCE([Jan], 0)&lt;/code&gt; in outer projection. BigQuery: &lt;code&gt;COALESCE(Jan, 0)&lt;/code&gt; in outer projection. Portable: &lt;code&gt;COALESCE(SUM(CASE WHEN …), 0)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When to use PIVOT.&lt;/strong&gt; 6+ output columns, single dialect, single aggregate, static labels. When to use &lt;code&gt;CASE WHEN&lt;/code&gt;: portability, mixed aggregates, tiny column sets, debuggability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MySQL emulation.&lt;/strong&gt; No &lt;code&gt;PIVOT&lt;/code&gt;, no &lt;code&gt;UNPIVOT&lt;/code&gt;. Use &lt;code&gt;SUM(CASE WHEN …)&lt;/code&gt; for pivot, &lt;code&gt;UNION ALL&lt;/code&gt; or &lt;code&gt;JSON_TABLE&lt;/code&gt; (8.0.4+) for unpivot. Stored procedures with dynamic SQL for compile-time-unknown labels.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does Postgres support &lt;code&gt;PIVOT&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;Not with the &lt;code&gt;PIVOT&lt;/code&gt; keyword — Postgres has no native &lt;code&gt;PIVOT&lt;/code&gt; clause. Postgres teams have three options: (1) the &lt;code&gt;tablefunc&lt;/code&gt; extension's &lt;code&gt;crosstab(query, categories)&lt;/code&gt; function, which is the classical answer but requires declaring output column names and types explicitly in &lt;code&gt;AS ct(…)&lt;/code&gt;; (2) &lt;code&gt;SUM(CASE WHEN month = 'Jan' THEN sales END) AS jan&lt;/code&gt;, the universal portable recipe that works on every dialect; (3) the ANSI &lt;code&gt;FILTER (WHERE …)&lt;/code&gt; clause supported by Postgres 9.4+, which reads slightly more naturally than &lt;code&gt;CASE WHEN&lt;/code&gt; and plans identically. For any Postgres query you might port to another warehouse, use conditional aggregation. For Postgres-only reports where the column count is high, &lt;code&gt;crosstab&lt;/code&gt; remains readable.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between &lt;code&gt;PIVOT&lt;/code&gt; and &lt;code&gt;crosstab&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;PIVOT&lt;/code&gt; is a SQL clause in a &lt;code&gt;FROM&lt;/code&gt; context that shapes a subquery — it treats every implicit column as a grouping key and expands one column into many. &lt;code&gt;crosstab&lt;/code&gt; is a Postgres &lt;code&gt;tablefunc&lt;/code&gt; function that takes two SQL strings: one for the long-form input (must be &lt;code&gt;ORDER BY row_key, category&lt;/code&gt;) and one for the category order pin list. &lt;code&gt;crosstab&lt;/code&gt; requires you to declare the output column list and types in an &lt;code&gt;AS ct(row_key type, cat_a type, …)&lt;/code&gt; alias; &lt;code&gt;PIVOT&lt;/code&gt; infers them from the label list. Functionally the two produce identical output for the same input; syntactically &lt;code&gt;crosstab&lt;/code&gt; is more verbose and has stricter ordering requirements. If you're on Snowflake, BigQuery, or SQL Server, use &lt;code&gt;PIVOT&lt;/code&gt;; if you're on Postgres with &lt;code&gt;tablefunc&lt;/code&gt; installed, &lt;code&gt;crosstab&lt;/code&gt; works but many teams still prefer &lt;code&gt;SUM(CASE WHEN …)&lt;/code&gt; for portability.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does BigQuery &lt;code&gt;PIVOT&lt;/code&gt; work with dynamic column lists?
&lt;/h3&gt;

&lt;p&gt;Not directly — &lt;code&gt;bigquery pivot&lt;/code&gt; requires a static &lt;code&gt;IN&lt;/code&gt; list in the pivot clause. To handle unknown labels at query time, wrap the pivot in a BigQuery scripting block: declare a &lt;code&gt;STRING&lt;/code&gt; variable, populate it by running &lt;code&gt;STRING_AGG(DISTINCT CONCAT("'", pivot_col, "'") ORDER BY pivot_col)&lt;/code&gt; against the source, then &lt;code&gt;EXECUTE IMMEDIATE FORMAT("SELECT * FROM t PIVOT(SUM(val) FOR col IN (%s))", cols)&lt;/code&gt;. Always use &lt;code&gt;USING @param&lt;/code&gt; binding when the label list depends on user input to avoid SQL injection. The compile-time alternative — a dbt macro that discovers labels with &lt;code&gt;run_query()&lt;/code&gt; and emits static &lt;code&gt;SUM(CASE WHEN …)&lt;/code&gt; — is preferred for analytics workflows because the resulting SQL is testable, cacheable, and dialect-portable.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I &lt;code&gt;UNPIVOT&lt;/code&gt; in MySQL?
&lt;/h3&gt;

&lt;p&gt;MySQL has no &lt;code&gt;UNPIVOT&lt;/code&gt; keyword. The two idiomatic answers are &lt;code&gt;UNION ALL&lt;/code&gt; (works on every MySQL version) and &lt;code&gt;JSON_TABLE&lt;/code&gt; (MySQL 8.0.4+). For &lt;code&gt;UNION ALL&lt;/code&gt;, write one &lt;code&gt;SELECT&lt;/code&gt; per unpivoted column: &lt;code&gt;SELECT product, 'jan' AS month, jan AS sales FROM t UNION ALL SELECT product, 'feb', feb FROM t …&lt;/code&gt;. For &lt;code&gt;JSON_TABLE&lt;/code&gt;, build a JSON array of label-value pairs and join against it: &lt;code&gt;SELECT t.product, j.month, j.sales FROM t, JSON_TABLE(JSON_ARRAY(JSON_OBJECT('month', 'jan', 'sales', t.jan), JSON_OBJECT('month', 'feb', 'sales', t.feb)), '$[*]' COLUMNS (month VARCHAR(3) PATH '$.month', sales INT PATH '$.sales')) AS j&lt;/code&gt;. &lt;code&gt;UNION ALL&lt;/code&gt; reads more clearly for 3–5 columns; &lt;code&gt;JSON_TABLE&lt;/code&gt; scales better beyond that. For any wider unpivot, generate the SQL in application code — the boilerplate savings pay for the templating complexity.&lt;/p&gt;

&lt;h3&gt;
  
  
  When should I use conditional aggregation instead of &lt;code&gt;PIVOT&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;Three cases push you to &lt;code&gt;SUM(CASE WHEN …)&lt;/code&gt; over &lt;code&gt;PIVOT&lt;/code&gt;. First, &lt;strong&gt;portability&lt;/strong&gt; — one query that runs identically on Postgres AND Snowflake AND BigQuery AND SQL Server AND MySQL cannot use &lt;code&gt;PIVOT&lt;/code&gt; because Postgres and MySQL have no such keyword. Second, &lt;strong&gt;mixed aggregates&lt;/strong&gt; in a single output row — &lt;code&gt;SUM + COUNT + MAX + AVG&lt;/code&gt; per label combination is not expressible with &lt;code&gt;PIVOT&lt;/code&gt; (which allows only one aggregate) but is a one-line pattern with conditional aggregation. Third, &lt;strong&gt;tiny column sets&lt;/strong&gt; (3–5 outputs) where &lt;code&gt;PIVOT&lt;/code&gt;'s boilerplate savings are small and the debuggability of a plain &lt;code&gt;SELECT&lt;/code&gt; + &lt;code&gt;GROUP BY&lt;/code&gt; wins. Reach for &lt;code&gt;PIVOT&lt;/code&gt; when the output has 6+ columns and readability is the main win, or when you're using Snowflake's flagship &lt;code&gt;ANY ORDER BY&lt;/code&gt; dynamic form. Every dialect compiles &lt;code&gt;PIVOT&lt;/code&gt; to the same plan as &lt;code&gt;SUM(CASE WHEN …)&lt;/code&gt;, so performance is never the deciding factor.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the &lt;code&gt;FILTER (WHERE …)&lt;/code&gt; clause and where does it work?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;FILTER (WHERE …)&lt;/code&gt; is the ANSI SQL:2003 clause that post-filters rows for a specific aggregate. Syntax: &lt;code&gt;SUM(sales) FILTER (WHERE month = 'Jan') AS jan&lt;/code&gt;. It reads more naturally than &lt;code&gt;SUM(CASE WHEN month = 'Jan' THEN sales END)&lt;/code&gt; and plans identically — the query optimiser produces the exact same execution plan for both forms. Supported dialects in 2026: Postgres 9.4+, SQLite 3.30+, DuckDB, and Firebird. NOT supported: Snowflake, BigQuery, SQL Server, MySQL, MariaDB, Oracle. On Postgres, &lt;code&gt;FILTER&lt;/code&gt; also works with window functions: &lt;code&gt;SUM(sales) FILTER (WHERE month = 'Jan') OVER (PARTITION BY region)&lt;/code&gt; — a niche superpower. For any Postgres-only or SQLite-only codebase, adopt &lt;code&gt;FILTER&lt;/code&gt; as the house style; for a portable library, fall back to &lt;code&gt;SUM(CASE WHEN …)&lt;/code&gt; because it works everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practice on PipeCode
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Drill the &lt;a href="https://pipecode.ai/explore/practice/topic/unpivoting" rel="noopener noreferrer"&gt;unpivoting practice library →&lt;/a&gt; for the wide-to-long reshape family — &lt;code&gt;UNPIVOT&lt;/code&gt;, &lt;code&gt;UNION ALL&lt;/code&gt;, and the Postgres jsonb one-liner.&lt;/li&gt;
&lt;li&gt;Rehearse on &lt;a href="https://pipecode.ai/explore/practice/topic/conditional-aggregation" rel="noopener noreferrer"&gt;conditional-aggregation problems →&lt;/a&gt; when the interviewer wants a portable &lt;code&gt;SUM(CASE WHEN …)&lt;/code&gt; pivot.&lt;/li&gt;
&lt;li&gt;Sharpen &lt;a href="https://pipecode.ai/explore/practice/topic/grouping-sets" rel="noopener noreferrer"&gt;grouping-sets drills →&lt;/a&gt; for &lt;code&gt;ROLLUP&lt;/code&gt;, &lt;code&gt;CUBE&lt;/code&gt;, and multi-level aggregation reports adjacent to pivot patterns.&lt;/li&gt;
&lt;li&gt;Stack the &lt;a href="https://pipecode.ai/explore/practice/topic/aggregation" rel="noopener noreferrer"&gt;aggregation library →&lt;/a&gt; for &lt;code&gt;SUM&lt;/code&gt; / &lt;code&gt;COUNT&lt;/code&gt; / &lt;code&gt;MAX&lt;/code&gt; / &lt;code&gt;MIN&lt;/code&gt; / &lt;code&gt;AVG&lt;/code&gt; interview probes that underpin every pivot.&lt;/li&gt;
&lt;li&gt;Layer the &lt;a href="https://pipecode.ai/explore/practice/topic/case-expression" rel="noopener noreferrer"&gt;CASE expression library →&lt;/a&gt; for the conditional-aggregation primitive that powers portable pivots.&lt;/li&gt;
&lt;li&gt;Complement the reshape grind with the &lt;a href="https://pipecode.ai/explore/practice/topic/case-when" rel="noopener noreferrer"&gt;CASE WHEN topic collection →&lt;/a&gt; for shorter warm-up drills.&lt;/li&gt;
&lt;li&gt;For general SQL sharpening, work through the &lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;SQL problem library →&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;For the broader data-engineering surface, read &lt;a href="https://pipecode.ai/blogs/top-data-engineering-interview-questions-2026" rel="noopener noreferrer"&gt;top data engineering interview questions →&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Stack the prerequisites with &lt;a href="https://pipecode.ai/blogs/the-only-5-skills-you-need-to-become-a-data-engineer" rel="noopener noreferrer"&gt;the only 5 skills you need to become a data engineer →&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;For the broader SQL interview surface, take the &lt;a href="https://pipecode.ai/explore/courses/sql-for-data-engineering-interviews-from-zero-to-faang" rel="noopener noreferrer"&gt;SQL for Data Engineering course →&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/" rel="noopener noreferrer"&gt;Pipecode.ai&lt;/a&gt; is Leetcode for Data Engineering — every PIVOT, UNPIVOT, and conditional-aggregation recipe above ships with hands-on practice rooms where you write the Snowflake `PIVOT`, wire the Postgres `crosstab`, unpivot with `UNION ALL`, and pick the portable `SUM(CASE WHEN …)` against real graded inputs. PipeCode pairs every reading with 450+ DE-focused problems and a real-time scoring engine, so you never have to wonder whether your `sql pivot` answer holds up under a senior interviewer's depth probes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/unpivoting" rel="noopener noreferrer"&gt;Practice pivot patterns now →&lt;/a&gt;&lt;br&gt;
&lt;a href="https://pipecode.ai/explore/practice/topic/conditional-aggregation" rel="noopener noreferrer"&gt;Conditional aggregation drills →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>sql</category>
      <category>interview</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>SQL Recursive CTEs: Hierarchies, Trees, Graph Traversal, Bill-of-Materials</title>
      <dc:creator>Gowtham Potureddi</dc:creator>
      <pubDate>Fri, 10 Jul 2026 17:20:37 +0000</pubDate>
      <link>https://dev.to/gowthampotureddi/sql-recursive-ctes-hierarchies-trees-graph-traversal-bill-of-materials-55od</link>
      <guid>https://dev.to/gowthampotureddi/sql-recursive-ctes-hierarchies-trees-graph-traversal-bill-of-materials-55od</guid>
      <description>&lt;p&gt;&lt;strong&gt;&lt;code&gt;sql recursive cte&lt;/code&gt;&lt;/strong&gt; is the one piece of SQL that turns "impossible without a loop" into "one query with a &lt;code&gt;WITH RECURSIVE&lt;/code&gt;." Org-chart traversal, category-tree explosion, friendship-graph BFS, multi-level bill-of-materials rollup — every one of these used to be a Python script pulling rows in batches until a manager column was &lt;code&gt;NULL&lt;/code&gt;. Since 2018 or so, every serious dialect has shipped a standards-compliant recursive CTE. The engineering trade-off is no longer "can I do this in SQL?" but "which anchor, which recursive term, and where does the termination live?"&lt;/p&gt;

&lt;p&gt;This guide is the mid-to-senior data-engineer walkthrough you wished existed the first time an interviewer asked you to write a &lt;code&gt;recursive query sql&lt;/code&gt; on the whiteboard, sketch a &lt;code&gt;sql hierarchy query&lt;/code&gt; for an org chart, roll up a &lt;code&gt;bill of materials sql&lt;/code&gt;, or run a &lt;code&gt;graph traversal sql&lt;/code&gt; with cycle detection. It walks through the recursive CTE anatomy (anchor + recursive term + &lt;code&gt;UNION ALL&lt;/code&gt; + termination + dialect keyword differences), single-parent hierarchies (employee-manager tree, depth level column, path array, &lt;code&gt;ORDER BY&lt;/code&gt; sibling column, category tree), multi-parent graph traversal (undirected friendship graph BFS, visited-path arrays, cycle detection with &lt;code&gt;NOT ... = ANY(path)&lt;/code&gt;, Postgres &lt;code&gt;SEARCH BREADTH FIRST&lt;/code&gt; / &lt;code&gt;SEARCH DEPTH FIRST&lt;/code&gt; and &lt;code&gt;CYCLE&lt;/code&gt; clauses), bill-of-materials rollup with quantity multiplication, and the 5-dialect max-recursion matrix (Postgres / MySQL 8 / SQL Server / Snowflake / BigQuery). Each section pairs a teaching block with a Solution-Tail interview answer — code, a step-by-step trace, an output table, then a concept-by-concept breakdown of why it works.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqrjf20gzezcldjw2tqj7.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqrjf20gzezcldjw2tqj7.jpeg" alt="PipeCode blog header for SQL recursive CTEs — bold white headline 'SQL Recursive CTEs' with subtitle 'Hierarchies · Trees · Graph Traversal · BOM' and a stylised WITH RECURSIVE anchor+recursive tree diagram on a dark gradient with purple, green, orange, and blue accents and a small pipecode.ai attribution." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you want &lt;strong&gt;hands-on reps&lt;/strong&gt; immediately after reading, drill the &lt;a href="https://pipecode.ai/explore/practice/topic/cte" rel="noopener noreferrer"&gt;CTE practice library →&lt;/a&gt;, rehearse on &lt;a href="https://pipecode.ai/explore/practice/topic/cte/difficulty/hard" rel="noopener noreferrer"&gt;hard-difficulty CTE problems →&lt;/a&gt;, and sharpen the SQL axis with the &lt;a href="https://pipecode.ai/explore/practice/topic/cte/sql" rel="noopener noreferrer"&gt;SQL CTE drills →&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;On this page&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why recursive CTEs matter in 2026&lt;/li&gt;
&lt;li&gt;Recursive CTE anatomy&lt;/li&gt;
&lt;li&gt;Hierarchies and trees — org chart / category tree&lt;/li&gt;
&lt;li&gt;Graph traversal and cycle detection&lt;/li&gt;
&lt;li&gt;Bill-of-materials and dialect quirks&lt;/li&gt;
&lt;li&gt;Cheat sheet — recursive CTE recipes&lt;/li&gt;
&lt;li&gt;Frequently asked questions&lt;/li&gt;
&lt;li&gt;Practice on PipeCode&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1. Why recursive CTEs matter in 2026
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Recursive CTEs replace app-side loops for hierarchies, trees, graphs, and BOMs — one query, no round-trips
&lt;/h3&gt;

&lt;p&gt;The one-sentence invariant: &lt;strong&gt;a &lt;code&gt;sql recursive cte&lt;/code&gt; walks a self-referential structure inside the database by repeatedly joining a "frontier" to the parent table, so the shape of the answer is not "loop in Python, one query per level" but "one query, one plan, one pass through the buffer pool"&lt;/strong&gt;. Every other consequence — depth column, path array, cycle detection, BOM rollup — is a specific application of the same anchor-plus-recursive-term shape. Once you internalise the shape, &lt;code&gt;recursive query sql&lt;/code&gt; becomes a template you fill in rather than an algorithm you invent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three axes interviewers actually probe.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Structure.&lt;/strong&gt; A &lt;code&gt;sql hierarchy query&lt;/code&gt; walks a single-parent tree (&lt;code&gt;manager_id&lt;/code&gt; points to at most one manager). A &lt;code&gt;graph traversal sql&lt;/code&gt; walks a multi-parent DAG or cyclic graph (a friendship edge table where any node can point to many). A recursive CTE handles both; the second requires cycle detection and the first does not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Termination.&lt;/strong&gt; The recursive term must produce zero rows eventually, or the plan runs until the dialect's max-recursion setting fires. For a tree, termination is automatic (leaves have no children). For a graph, termination is engineered — a visited-path array plus a &lt;code&gt;NOT visited&lt;/code&gt; predicate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollup.&lt;/strong&gt; BOM rollup multiplies quantities along every edge: &lt;code&gt;child_accumulated = parent_accumulated × this_edge_qty&lt;/code&gt;. The recursive term does the arithmetic; the outer aggregate sums per component. Interviewers love to see the multiplication happen inside the CTE, not after.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Hierarchies vs graphs — the structural split.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Single-parent tree.&lt;/strong&gt; Employee-manager, filesystem, category tree, comment thread. Each row points to at most one parent. Cycle detection is &lt;em&gt;not required&lt;/em&gt; because a tree by definition cannot have cycles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-parent DAG.&lt;/strong&gt; BOM (a wheel appears in a bike and a scooter), permission chain (a role inherits from multiple parent roles), lineage graph (a dataset has multiple upstream sources). Cycles are impossible by construction but paths can revisit the same node — dedup at the outer query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cyclic graph.&lt;/strong&gt; Social network, biological pathway, dependency graph with feedback. Cycles are possible; cycle detection is &lt;em&gt;mandatory&lt;/em&gt; to prevent infinite recursion.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Dialect status matrix — 2026 reality.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Postgres&lt;/strong&gt; — first-class since 8.4 (2009). &lt;code&gt;WITH RECURSIVE&lt;/code&gt;, &lt;code&gt;SEARCH BREADTH FIRST / DEPTH FIRST&lt;/code&gt;, &lt;code&gt;CYCLE&lt;/code&gt; clause. Deepest feature set of any mainstream engine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MySQL 8+&lt;/strong&gt; — recursive CTE since 8.0 (2018). &lt;code&gt;WITH RECURSIVE&lt;/code&gt; required. &lt;code&gt;cte_max_recursion_depth&lt;/code&gt; default 1000. No &lt;code&gt;SEARCH&lt;/code&gt; or &lt;code&gt;CYCLE&lt;/code&gt; clauses — roll your own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL Server&lt;/strong&gt; — recursive CTE since 2005. &lt;code&gt;WITH&lt;/code&gt; (recursion inferred from the self-reference). Default &lt;code&gt;MAXRECURSION 100&lt;/code&gt;, override per-query with &lt;code&gt;OPTION (MAXRECURSION N)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snowflake&lt;/strong&gt; — supported. &lt;code&gt;WITH RECURSIVE&lt;/code&gt;. Effectively unbounded depth; watch cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BigQuery&lt;/strong&gt; — supported since 2022. &lt;code&gt;WITH RECURSIVE&lt;/code&gt;. Default 500 iterations, no per-query override — pre-flatten deeper structures or move to a graph DB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DuckDB&lt;/strong&gt; — first-class since 0.7. &lt;code&gt;WITH RECURSIVE&lt;/code&gt; and effective for local analytics.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Where a recursive CTE replaces app-side code.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python loop that hits the DB N times.&lt;/strong&gt; Any code that walks a parent chain by issuing "SELECT * FROM t WHERE id = ?" in a while loop is a recursive CTE waiting to happen. One query, one round trip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ORM &lt;code&gt;select_related&lt;/code&gt; chains.&lt;/strong&gt; Django / SQLAlchemy &lt;code&gt;select_related&lt;/code&gt; cannot follow a self-referential foreign key of unknown depth. Recursive CTE fixes it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Materialised transitive closure.&lt;/strong&gt; A "closure table" (one row per ancestor-descendant pair) can be regenerated from a recursive CTE nightly, without maintaining triggers or app-side denormalisation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Graph DB for shallow depth.&lt;/strong&gt; Neo4j / TigerGraph shine at depth &amp;gt; 5 with millions of nodes. For depth ≤ 4 or nodes ≤ 100K, a recursive CTE against Postgres is usually cheaper and simpler.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What interviewers listen for.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do you say &lt;strong&gt;"anchor + recursive term + &lt;code&gt;UNION ALL&lt;/code&gt; + termination"&lt;/strong&gt; in the first sentence? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you distinguish &lt;strong&gt;"single-parent tree (no cycle guard needed)" vs "multi-parent or cyclic graph (cycle guard mandatory)"&lt;/strong&gt;? — required answer.&lt;/li&gt;
&lt;li&gt;Do you mention &lt;strong&gt;max-recursion overrides per dialect&lt;/strong&gt; (MySQL &lt;code&gt;cte_max_recursion_depth&lt;/code&gt;, SQL Server &lt;code&gt;MAXRECURSION&lt;/code&gt;) unprompted? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you push back on &lt;strong&gt;"just use a graph database"&lt;/strong&gt; with "recursive CTE is the right answer for shallow depth and small node count"? — senior signal.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Worked example — a single query that would take a Python loop
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The problem shape.&lt;/strong&gt; Given an &lt;code&gt;employees&lt;/code&gt; table with &lt;code&gt;(id, name, manager_id)&lt;/code&gt;, list every employee under CEO Alice with their depth from the top. The naive shape is a Python loop: fetch direct reports of Alice, then their direct reports, then their direct reports, joining a level column each time. A recursive CTE does the same walk in one query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why one query wins.&lt;/strong&gt; Each Python-loop iteration pays a full network round-trip. The recursive CTE runs entirely inside the executor with a single plan, materialising the intermediate frontier in a worktable. Latency drops from O(depth × round-trip) to O(single query).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What the recursive CTE looks like.&lt;/strong&gt; Anchor selects Alice (&lt;code&gt;WHERE manager_id IS NULL&lt;/code&gt;). Recursive term joins &lt;code&gt;employees&lt;/code&gt; to the CTE by matching &lt;code&gt;employees.manager_id = cte.id&lt;/code&gt;, adding a &lt;code&gt;level + 1&lt;/code&gt;. Terminates when a level has no direct reports.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Rewrite the Python "walk the org chart" loop as a single recursive CTE. Return &lt;code&gt;employee_id&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;level&lt;/code&gt; (root = 0), and sort depth-first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;manager_id&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Alice&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Bob&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Carol&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Dan&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Eve&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Frank&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Grace&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;RECURSIVE&lt;/span&gt; &lt;span class="n"&gt;org_chart&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="c1"&gt;-- anchor: the CEO&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;manager_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;level&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ARRAY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;manager_id&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;

    &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;

    &lt;span class="c1"&gt;-- recursive term: children of the current frontier&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;manager_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;oc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;level&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;oc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
    &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;org_chart&lt;/span&gt; &lt;span class="n"&gt;oc&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;manager_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;oc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;level&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;org_chart&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Anchor.&lt;/strong&gt; Runs once. Selects Alice — the only row where &lt;code&gt;manager_id IS NULL&lt;/code&gt;. Materialises the initial frontier &lt;code&gt;{Alice}&lt;/code&gt; with &lt;code&gt;level = 0&lt;/code&gt; and &lt;code&gt;path = [1]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recursive step 1.&lt;/strong&gt; Joins the frontier &lt;code&gt;{Alice}&lt;/code&gt; to &lt;code&gt;employees&lt;/code&gt; on &lt;code&gt;e.manager_id = oc.id&lt;/code&gt; (i.e. &lt;code&gt;manager_id = 1&lt;/code&gt;). Emits Bob and Carol with &lt;code&gt;level = 1&lt;/code&gt; and paths &lt;code&gt;[1, 2]&lt;/code&gt;, &lt;code&gt;[1, 3]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recursive step 2.&lt;/strong&gt; New frontier is &lt;code&gt;{Bob, Carol}&lt;/code&gt;. Joining on &lt;code&gt;manager_id IN (2, 3)&lt;/code&gt; emits Dan (&lt;code&gt;level=2&lt;/code&gt;, &lt;code&gt;[1,2,4]&lt;/code&gt;), Eve (&lt;code&gt;level=2&lt;/code&gt;, &lt;code&gt;[1,2,5]&lt;/code&gt;), Frank (&lt;code&gt;level=2&lt;/code&gt;, &lt;code&gt;[1,3,6]&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recursive step 3.&lt;/strong&gt; Frontier &lt;code&gt;{Dan, Eve, Frank}&lt;/code&gt; produces Grace (&lt;code&gt;level=3&lt;/code&gt;, &lt;code&gt;[1,2,4,7]&lt;/code&gt;). No other children exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recursive step 4.&lt;/strong&gt; Frontier is &lt;code&gt;{Grace}&lt;/code&gt;. Nobody reports to Grace → recursive term returns zero rows → CTE terminates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outer query.&lt;/strong&gt; Selects every accumulated row and orders by &lt;code&gt;path&lt;/code&gt;, which renders the tree in depth-first order (Alice → Bob → Dan → Grace → Eve → Carol → Frank).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;level&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Alice&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Bob&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Dan&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Grace&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Eve&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Carol&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Frank&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Any time you catch yourself writing a &lt;code&gt;while row.parent_id is not None&lt;/code&gt; loop in application code against a table with a self-referential FK, stop and write a recursive CTE. One query beats N round-trips every time.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — hierarchies vs graphs, side by side
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A hierarchy is a special graph.&lt;/strong&gt; A tree is a graph where every node has at most one parent and the reachability from the root has no cycle. A recursive CTE against a tree never revisits a node.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A graph can have cycles.&lt;/strong&gt; A friendship edge table can encode &lt;code&gt;alice → bob&lt;/code&gt;, &lt;code&gt;bob → carol&lt;/code&gt;, &lt;code&gt;carol → alice&lt;/code&gt;. Without a cycle guard, the recursive CTE loops forever until &lt;code&gt;MAXRECURSION&lt;/code&gt; fires.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The syntactic split is small.&lt;/strong&gt; The only difference between a tree walker and a graph walker is one extra &lt;code&gt;WHERE&lt;/code&gt; clause and one accumulated array. The mental model is identical.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given the same recursive CTE skeleton, show the delta between walking a tree and walking a potentially cyclic graph. Highlight the cycle-guard predicate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input — tree (employees).&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;manager_id&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Alice&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Bob&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Input — graph (friendships, undirected).&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;a&lt;/th&gt;
&lt;th&gt;b&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Tree walker — no cycle guard needed&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;RECURSIVE&lt;/span&gt; &lt;span class="n"&gt;org_chart&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;level&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;manager_id&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
    &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;oc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;level&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
    &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;org_chart&lt;/span&gt; &lt;span class="n"&gt;oc&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;manager_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;oc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;org_chart&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Graph walker — CYCLE GUARD IS MANDATORY&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;RECURSIVE&lt;/span&gt; &lt;span class="n"&gt;friends&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;start_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;node_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;hop&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ARRAY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;
    &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node_id&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;node_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hop&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node_id&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;friends&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
    &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;friendships&lt;/span&gt; &lt;span class="n"&gt;fr&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node_id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node_id&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;ANY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;-- cycle guard&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;friends&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Tree.&lt;/strong&gt; The recursive term joins children of the frontier via &lt;code&gt;manager_id = oc.id&lt;/code&gt;. Because every child has exactly one manager, no row is ever emitted twice. Termination is automatic when the frontier hits leaves.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Graph.&lt;/strong&gt; The recursive term joins any edge incident to the frontier node. Without a guard, once we visit &lt;code&gt;1 → 2 → 3&lt;/code&gt;, the next step matches &lt;code&gt;3 → 1&lt;/code&gt; and we start the cycle again. The &lt;code&gt;WHERE NOT (... = ANY(f.path))&lt;/code&gt; predicate blocks re-visiting a node.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;path&lt;/code&gt; array is the memory.&lt;/strong&gt; It records every node visited from the starting point along this particular walk. Two different walks from the same start can share prefixes but the path is per-row, not per-CTE-wide.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost of the guard.&lt;/strong&gt; Adding &lt;code&gt;= ANY(f.path)&lt;/code&gt; is an O(depth) linear scan per recursive step. For deep graphs, this is negligible compared to the join cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Termination.&lt;/strong&gt; With the guard in place, the recursive term returns zero rows once every reachable node has been visited from every reachable start. Same as the tree — the frontier eventually drains.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output — tree walker.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;level&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Alice&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Bob&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Output — graph walker (BFS from node 1).&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;start_id&lt;/th&gt;
&lt;th&gt;node_id&lt;/th&gt;
&lt;th&gt;hop&lt;/th&gt;
&lt;th&gt;path&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;[1]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;[1, 2]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;[1, 2, 3]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; If the structure guarantees a single-parent tree, skip the cycle guard. If the structure has any multi-parent or bidirectional edges, add the guard as a matter of course — even if you "know" the data is cycle-free today, tomorrow's bug will insert one.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — when NOT to use a recursive CTE
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Depth &amp;gt; 1000 with millions of nodes.&lt;/strong&gt; Recursive CTE cost is O(edges × depth × node_count in the worktable). At millions of nodes and depth &amp;gt; 10, the plan gets expensive fast. Move to a graph DB or a materialised closure table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read-heavy workload with rare structural changes.&lt;/strong&gt; If the tree changes weekly but the query runs every second, materialise a closure table once and query it directly. O(1) lookup vs O(depth) recursion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BigQuery hard limits.&lt;/strong&gt; BigQuery's 500-iteration cap and per-slot cost model make deep recursive CTEs pricy; pre-flatten with a dbt materialised transitive closure model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need shortest path, not any path.&lt;/strong&gt; A recursive CTE finds paths; it does not naturally give the shortest. Cypher / Gremlin do; a recursive CTE with &lt;code&gt;ORDER BY hop LIMIT 1&lt;/code&gt; works but wastes IO.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given a 20M-node, average-depth-15 hierarchy queried 500 times per second, would you use a recursive CTE? What's the alternative?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Constraint&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Nodes&lt;/td&gt;
&lt;td&gt;20 M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Average depth&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read QPS&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Structural change QPS&lt;/td&gt;
&lt;td&gt;~1 per day&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Closure table — one row per (ancestor, descendant, depth) pair&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;org_closure&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ancestor_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;descendant_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;depth&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ancestor_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;descendant_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Rebuild from the recursive CTE nightly&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;org_closure&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;RECURSIVE&lt;/span&gt; &lt;span class="n"&gt;walk&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;ancestor_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;descendant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;depth&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;
    &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ancestor_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;walk&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;
    &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;manager_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;descendant_id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;walk&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- The 500 QPS read now hits a single index lookup, not a recursive CTE&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;descendant_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;org_closure&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;ancestor_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;root_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The closure table stores one row per &lt;code&gt;(ancestor, descendant, depth)&lt;/code&gt; pair — the &lt;em&gt;transitive closure&lt;/em&gt; of the hierarchy. For a tree with N nodes and average depth D, the table has roughly &lt;code&gt;N × D&lt;/code&gt; rows.&lt;/li&gt;
&lt;li&gt;Rebuilding is a single recursive CTE, run once per day (or triggered on structural change). The write cost is amortised.&lt;/li&gt;
&lt;li&gt;Reads become a single-index lookup: &lt;code&gt;WHERE ancestor_id = :root_id&lt;/code&gt; returns all descendants without any recursion at query time.&lt;/li&gt;
&lt;li&gt;For 500 QPS, the closure table is O(1) per read; the recursive CTE is O(depth × frontier size), roughly 100x slower.&lt;/li&gt;
&lt;li&gt;The trade-off is disk space (&lt;code&gt;N × D&lt;/code&gt; rows) and write amplification on structural changes. For a nightly-rebuilt table with 15-average depth, this is 300M rows for 20M nodes — a couple of GB, trivial on modern storage.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Read latency&lt;/th&gt;
&lt;th&gt;Write cost&lt;/th&gt;
&lt;th&gt;Storage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Recursive CTE at query time&lt;/td&gt;
&lt;td&gt;30-200 ms per read&lt;/td&gt;
&lt;td&gt;zero&lt;/td&gt;
&lt;td&gt;zero&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Closure table + nightly rebuild&lt;/td&gt;
&lt;td&gt;1-5 ms per read&lt;/td&gt;
&lt;td&gt;one recursive CTE / day&lt;/td&gt;
&lt;td&gt;~N × D rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Graph DB (Neo4j)&lt;/td&gt;
&lt;td&gt;1-10 ms per read&lt;/td&gt;
&lt;td&gt;continuous&lt;/td&gt;
&lt;td&gt;native graph store&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Use a recursive CTE at query time for depth ≤ 10 with reads ≤ 10 QPS. Materialise a closure table for depth &amp;gt; 10 or reads &amp;gt; 100 QPS. Move to a graph DB only when both depth and QPS are high and the structural change rate is comparable to the read rate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on recursive CTE fit
&lt;/h3&gt;

&lt;p&gt;A senior interviewer often opens with: "When would you reach for a recursive CTE, and when would you push back and say 'not in SQL'? Walk me through the 3-4 questions you ask, in order, and what answers push you one way or the other."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a 4-question decision framework
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Recursive CTE fit — decision framework
======================================

1. Is the structure self-referential?
   - yes (parent_id, edges table)  → recursive CTE eligible
   - no (flat facts)               → not applicable

2. What is the max depth?
   - ≤ 10 (org chart, category tree) → recursive CTE fine
   - 10–100 (deep BOM)               → recursive CTE + max-recursion override
   - &amp;gt; 100 (very deep DAG)           → closure table or graph DB

3. What is the read QPS?
   - ≤ 10 QPS  → recursive CTE at query time
   - 10–100    → recursive CTE + result cache (Redis / materialised view)
   - &amp;gt; 100     → closure table (rebuilt nightly) or graph DB

4. Are cycles possible?
   - no (tree by construction)  → no cycle guard needed
   - yes (graph, feedback)      → cycle guard + path array mandatory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use case&lt;/th&gt;
&lt;th&gt;Q1 self-ref&lt;/th&gt;
&lt;th&gt;Q2 depth&lt;/th&gt;
&lt;th&gt;Q3 read QPS&lt;/th&gt;
&lt;th&gt;Q4 cycles&lt;/th&gt;
&lt;th&gt;Picked&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Small org chart (500 people)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;recursive CTE at query time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Product category tree (5k rows)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;closure table + rebuild on change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Friendship graph (10M users)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;∞&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;graph DB or 2-hop closure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-level BOM (aerospace)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;no (DAG)&lt;/td&gt;
&lt;td&gt;recursive CTE + MAXRECURSION override&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dataset lineage graph&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;possible&lt;/td&gt;
&lt;td&gt;recursive CTE + cycle guard&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After the 4-question pass, the fit is usually unambiguous. The remaining edge cases — where recursive CTE, closure table, and graph DB all work — default to whatever the team already operates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;When it wins&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Recursive CTE at query time&lt;/td&gt;
&lt;td&gt;Shallow depth, low-to-medium QPS, tree or bounded DAG&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Closure table + nightly rebuild&lt;/td&gt;
&lt;td&gt;Medium depth, high QPS, low structural change rate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Graph database (Neo4j, TigerGraph)&lt;/td&gt;
&lt;td&gt;Deep, cyclic, and high-QPS at the same time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Self-referential structure — the gate&lt;/strong&gt;&lt;/strong&gt; — recursive CTE is only relevant when the data model has a self-reference (a foreign key back to the same table or an edges table). Flat facts do not need recursion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Depth budget&lt;/strong&gt;&lt;/strong&gt; — every dialect has a default max recursion. Postgres is effectively unbounded, MySQL is 1000, SQL Server is 100, BigQuery is 500. Depth greater than the default forces an override or a redesign.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;QPS budget&lt;/strong&gt;&lt;/strong&gt; — recursive CTEs are O(depth × frontier size) per query. High-QPS reads amortise better against a materialised closure table or a graph DB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cycle possibility&lt;/strong&gt;&lt;/strong&gt; — trees do not need a guard; graphs always do. Add the guard whenever the structure has multi-parent or bidirectional edges — even if today's data is clean.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — recursive CTE at query time is zero infrastructure; closure table adds one nightly job and disk space; graph DB adds a new system to operate. The three tiers scale with the complexity of the workload, not the elegance of the model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — CTE&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Recursive CTE problems (all difficulty)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/cte" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — CTE · SQL&lt;/span&gt;
&lt;strong&gt;CTE problems in SQL&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/cte/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  2. Recursive CTE anatomy
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;WITH RECURSIVE cte AS (anchor UNION ALL recursive_term) SELECT ... FROM cte&lt;/code&gt; — the only shape you ever need
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;a recursive CTE has exactly two SELECTs stitched by &lt;code&gt;UNION ALL&lt;/code&gt; — the anchor runs once and seeds a "frontier"; the recursive term runs repeatedly, each time joining &lt;code&gt;cte&lt;/code&gt; to a base table until it returns zero rows&lt;/strong&gt;. Once you say that out loud, every &lt;code&gt;sql cte&lt;/code&gt; interview question about recursion becomes a template you fill in.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzck68tuhu4kffktkgxd1.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzck68tuhu4kffktkgxd1.jpeg" alt="Visual diagram of a recursive CTE anatomy — left an anchor SELECT card labelled 'base case', a UNION ALL connector in the middle, right a recursive SELECT card labelled 'recursive term joining back to the CTE'; a small termination pill and a dialect keyword strip showing WITH RECURSIVE across Postgres, MySQL 8, SQL Server, Snowflake, BigQuery; on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The three moving parts.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Anchor SELECT.&lt;/strong&gt; The base case. Runs once. Materialises the first "generation" of rows into the CTE's worktable. The anchor is the only place where the CTE's column list is &lt;em&gt;established&lt;/em&gt; — the recursive term's column list must match exactly (types included).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;UNION ALL&lt;/code&gt; connector.&lt;/strong&gt; Fuses the anchor and the recursive term. Almost always &lt;code&gt;UNION ALL&lt;/code&gt; — you want every generation appended, not deduplicated at the CTE level. &lt;code&gt;UNION&lt;/code&gt; (implicit &lt;code&gt;DISTINCT&lt;/code&gt;) is legal in the standard but rare in practice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recursive term SELECT.&lt;/strong&gt; Joins &lt;code&gt;cte&lt;/code&gt; (self-reference) to a base table. On every iteration, the executor treats the &lt;em&gt;previous&lt;/em&gt; iteration's output as the "current" CTE contents; the recursive term joins that frontier to the base table and emits the next generation. Termination happens when the recursive term returns zero rows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The stream-vs-set duality.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Conceptually, the CTE contains the &lt;em&gt;union of all generations&lt;/em&gt; by the time the outer query reads from it.&lt;/li&gt;
&lt;li&gt;Operationally, the executor materialises the anchor's rows into a worktable, then iterates: read from the worktable, join to base, insert result back into the worktable, repeat.&lt;/li&gt;
&lt;li&gt;The outer query sees the union — same data as the conceptual model, materialised via the operational one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;UNION ALL&lt;/code&gt; vs &lt;code&gt;UNION&lt;/code&gt; — the dedup trade-off.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;UNION ALL&lt;/code&gt;&lt;/strong&gt; — preserves all rows. Faster (no sort/hash). The right default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;UNION&lt;/code&gt;&lt;/strong&gt; — implicit &lt;code&gt;SELECT DISTINCT&lt;/code&gt; on every step. Silently dedupes generations. Sometimes useful for cyclic graphs but a cycle guard is usually better.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Practical rule.&lt;/strong&gt; Always start with &lt;code&gt;UNION ALL&lt;/code&gt;. If you need dedup, do it in the outer query with &lt;code&gt;SELECT DISTINCT&lt;/code&gt; where you can see it, not hidden inside the recursion.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Termination — the recursive term must eventually return zero rows.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tree termination.&lt;/strong&gt; Automatic. Leaves have no children; the frontier drains.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bounded numeric termination.&lt;/strong&gt; A &lt;code&gt;WHERE n &amp;lt; 10&lt;/code&gt; predicate on the recursive term forces zero rows once the counter passes the bound.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Graph termination.&lt;/strong&gt; The cycle guard (path predicate) removes already-visited nodes; the frontier drains once every reachable node is visited.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What happens without termination.&lt;/strong&gt; The dialect's max-recursion setting fires: MySQL errors at 1000, SQL Server at 100, Postgres runs until stack overflow. Never rely on the max-recursion to save you — it is a safety net, not a design.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Running numbers 1..N — the "hello world" that proves the shape.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The simplest possible recursive CTE. Anchor emits &lt;code&gt;1&lt;/code&gt;. Recursive term emits &lt;code&gt;n + 1 WHERE n &amp;lt; N&lt;/code&gt;. Terminates at &lt;code&gt;n = N&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Reveals the shape without any joins. Useful for generating date series, ID gaps analysis, cross-joining a schedule to a calendar.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Dialect keyword differences.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Postgres, MySQL 8+, Snowflake, BigQuery, DuckDB&lt;/strong&gt; — &lt;code&gt;WITH RECURSIVE cte AS (...)&lt;/code&gt;. The &lt;code&gt;RECURSIVE&lt;/code&gt; keyword is required.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL Server&lt;/strong&gt; — &lt;code&gt;WITH cte AS (...)&lt;/code&gt;. Recursion is inferred from the self-reference; no &lt;code&gt;RECURSIVE&lt;/code&gt; keyword. The rest of the shape is identical.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Oracle 11g+&lt;/strong&gt; — supports the standard &lt;code&gt;WITH cte(col) AS (anchor UNION ALL recursive_term)&lt;/code&gt; since 11gR2. Older Oracle uses &lt;code&gt;CONNECT BY&lt;/code&gt; syntax (proprietary; do not use in new code).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The generic template — memorise this.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;RECURSIVE&lt;/span&gt; &lt;span class="n"&gt;cte&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="c1"&gt;-- ANCHOR: base case, runs once&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;base_table&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;root_condition&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

    &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;

    &lt;span class="c1"&gt;-- RECURSIVE TERM: joins CTE to base table, runs until 0 rows&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;base_table&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;cte&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;termination_predicate&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;cte&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;...;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Common interview probes on anatomy.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"What is the difference between the anchor and the recursive term?" — anchor runs once and seeds the CTE; recursive term joins CTE to a base table and runs until zero rows.&lt;/li&gt;
&lt;li&gt;"Why &lt;code&gt;UNION ALL&lt;/code&gt; and not &lt;code&gt;UNION&lt;/code&gt;?" — &lt;code&gt;UNION ALL&lt;/code&gt; preserves rows; &lt;code&gt;UNION&lt;/code&gt; dedupes. Dedup at the CTE level is almost always the wrong place; do it in the outer query.&lt;/li&gt;
&lt;li&gt;"How do you terminate a recursive CTE?" — the recursive term must return zero rows. For trees, automatic. For counters, a &lt;code&gt;WHERE n &amp;lt; bound&lt;/code&gt;. For graphs, a cycle guard.&lt;/li&gt;
&lt;li&gt;"What if I omit &lt;code&gt;RECURSIVE&lt;/code&gt; in Postgres?" — you get a syntax error on the self-reference; Postgres does not infer recursion.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Worked example — running numbers 1..N
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Why start here.&lt;/strong&gt; Running numbers is the smallest recursive CTE — no joins, no path arrays, just a counter. It shows the shape (anchor + recursive + termination) with nothing else in the way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The termination predicate.&lt;/strong&gt; &lt;code&gt;WHERE n &amp;lt; 10&lt;/code&gt; on the recursive term. On the 10th iteration, &lt;code&gt;n = 10&lt;/code&gt;, so &lt;code&gt;n &amp;lt; 10&lt;/code&gt; is false → no rows → CTE terminates. The final CTE contains &lt;code&gt;n = 1, 2, ..., 10&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where this is useful.&lt;/strong&gt; Generating a date series (&lt;code&gt;WITH RECURSIVE dates AS (...) SELECT current_date + n * interval '1 day' FROM dates&lt;/code&gt;), building a slot table for calendar joins, generating a range for gaps-and-islands prep work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write a recursive CTE that generates the numbers 1 through 10 as a single-column result set. Show it in Postgres, MySQL 8, and SQL Server syntax.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(none — the CTE generates its own input)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Postgres / MySQL 8 / Snowflake / BigQuery — WITH RECURSIVE required&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;RECURSIVE&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;                        &lt;span class="c1"&gt;-- anchor&lt;/span&gt;
    &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;   &lt;span class="c1"&gt;-- recursive term&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- SQL Server — recursion inferred, no RECURSIVE keyword&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
    &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Iteration 0 (anchor).&lt;/strong&gt; Executes &lt;code&gt;SELECT 1&lt;/code&gt;. Worktable now has &lt;code&gt;{n=1}&lt;/code&gt;. This is the initial "current CTE contents."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Iteration 1 (recursive).&lt;/strong&gt; Executes &lt;code&gt;SELECT n + 1 FROM nums WHERE n &amp;lt; 10&lt;/code&gt; against the current CTE contents. &lt;code&gt;n = 1 &amp;lt; 10&lt;/code&gt;, so it emits &lt;code&gt;n = 2&lt;/code&gt;. Worktable accumulates &lt;code&gt;{n=1, n=2}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Iteration 2 (recursive).&lt;/strong&gt; The "current CTE" for this step is &lt;em&gt;just&lt;/em&gt; the previous iteration's output (&lt;code&gt;{n=2}&lt;/code&gt;), not the entire worktable. This is the SQL-standard semantics for recursive CTE — the recursive term joins the &lt;em&gt;incremental&lt;/em&gt; frontier, not the union. Emits &lt;code&gt;n = 3&lt;/code&gt;. Worktable now &lt;code&gt;{1, 2, 3}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Iterations 3-9.&lt;/strong&gt; Same shape. Emits &lt;code&gt;n = 4, 5, 6, 7, 8, 9, 10&lt;/code&gt;. Worktable accumulates through &lt;code&gt;n = 10&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Iteration 10.&lt;/strong&gt; Current frontier is &lt;code&gt;{n = 10}&lt;/code&gt;. &lt;code&gt;10 &amp;lt; 10&lt;/code&gt; is false → recursive term emits zero rows → CTE terminates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outer SELECT.&lt;/strong&gt; Reads all 10 rows from the accumulated worktable. Returns 1 through 10.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;n&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; The running-numbers pattern is the fastest way to sanity-check that a dialect supports recursive CTEs and that your syntax is right. If this 4-line query fails, you know to check the &lt;code&gt;RECURSIVE&lt;/code&gt; keyword and the anchor+recursive shape before touching anything more complex.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — &lt;code&gt;UNION ALL&lt;/code&gt; vs &lt;code&gt;UNION&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;UNION ALL&lt;/code&gt;&lt;/strong&gt; preserves every emitted row. The worktable grows monotonically; the recursive term joins the &lt;em&gt;current step's&lt;/em&gt; output to the base table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;UNION&lt;/code&gt;&lt;/strong&gt; performs an implicit &lt;code&gt;SELECT DISTINCT&lt;/code&gt; between the anchor and the recursive term. In practice, this means the executor dedupes across generations. If a generation would emit a row that already appeared in the worktable, that row is silently dropped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The trap.&lt;/strong&gt; In a cyclic graph, &lt;code&gt;UNION&lt;/code&gt; can &lt;em&gt;sometimes&lt;/em&gt; accidentally save you (the cycle produces a duplicate row that gets deduped), but it hides the bug rather than fixing it. Use a cycle guard, not &lt;code&gt;UNION&lt;/code&gt;, for graph traversal.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Show the difference between &lt;code&gt;UNION ALL&lt;/code&gt; and &lt;code&gt;UNION&lt;/code&gt; when the recursive term can re-emit the same row. Use a tiny 3-node cyclic graph.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input — edges (directed cycle).&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;src&lt;/th&gt;
&lt;th&gt;dst&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- UNION ALL — will run until MAXRECURSION fires&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;RECURSIVE&lt;/span&gt; &lt;span class="n"&gt;walk_all&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;start_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;node_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;hop&lt;/span&gt;
    &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dst&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hop&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;walk_all&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;
    &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;edges&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;walk_all&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- ERROR: max recursion 100 exhausted (SQL Server) / 1000 (MySQL)&lt;/span&gt;

&lt;span class="c1"&gt;-- UNION — implicit DISTINCT dedupes and (accidentally) terminates&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;RECURSIVE&lt;/span&gt; &lt;span class="n"&gt;walk_dist&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;start_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;node_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;hop&lt;/span&gt;
    &lt;span class="k"&gt;UNION&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dst&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hop&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;walk_dist&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;
    &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;edges&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;walk_dist&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;UNION ALL&lt;/code&gt; — infinite loop.&lt;/strong&gt; Anchor emits &lt;code&gt;{1}&lt;/code&gt;. Recursive step 1 emits &lt;code&gt;{2}&lt;/code&gt; (via edge &lt;code&gt;1→2&lt;/code&gt;). Step 2 emits &lt;code&gt;{3}&lt;/code&gt; (via &lt;code&gt;2→3&lt;/code&gt;). Step 3 emits &lt;code&gt;{1}&lt;/code&gt; (via &lt;code&gt;3→1&lt;/code&gt;) — but with a different &lt;code&gt;hop&lt;/code&gt; (3) than the anchor (0). Since &lt;code&gt;UNION ALL&lt;/code&gt; compares all columns, &lt;code&gt;(1, 1, 0)&lt;/code&gt; and &lt;code&gt;(1, 1, 3)&lt;/code&gt; are different rows; the executor keeps recursing forever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;UNION&lt;/code&gt; — silent dedup.&lt;/strong&gt; Same steps 1-3. Step 3 emits &lt;code&gt;(1, 1, 3)&lt;/code&gt;. &lt;code&gt;UNION&lt;/code&gt; compares — wait, is &lt;code&gt;(1, 1, 3)&lt;/code&gt; equal to &lt;code&gt;(1, 1, 0)&lt;/code&gt;? No, &lt;code&gt;hop&lt;/code&gt; differs. So the dedup still lets it through. If you drop &lt;code&gt;hop&lt;/code&gt; from the SELECT, then &lt;code&gt;UNION&lt;/code&gt; would collapse the cycle to a single row and terminate. But that also loses information you probably wanted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The lesson.&lt;/strong&gt; &lt;code&gt;UNION&lt;/code&gt; only terminates a cycle if the columns you SELECT collapse to already-seen values. Any &lt;code&gt;hop&lt;/code&gt; column, timestamp, or path array defeats the accidental termination.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The correct fix.&lt;/strong&gt; Add a cycle guard. Track visited nodes in a path array and filter with &lt;code&gt;NOT node_id = ANY(path)&lt;/code&gt;. Do not rely on &lt;code&gt;UNION&lt;/code&gt; for termination.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost.&lt;/strong&gt; &lt;code&gt;UNION&lt;/code&gt; costs an extra hash/sort per step to dedupe. On non-cyclic recursion, that overhead is pure waste — always use &lt;code&gt;UNION ALL&lt;/code&gt; unless you have measured &lt;code&gt;UNION&lt;/code&gt; and it is cheaper.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output — &lt;code&gt;UNION ALL&lt;/code&gt; on 3-cycle.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;start_id&lt;/th&gt;
&lt;th&gt;node_id&lt;/th&gt;
&lt;th&gt;hop&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;... (runs until MAXRECURSION)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Default to &lt;code&gt;UNION ALL&lt;/code&gt;. Reach for &lt;code&gt;UNION&lt;/code&gt; only when you have measured a specific dedup win and can articulate exactly which duplicate you are dropping. For graph traversal, use a cycle guard — never rely on &lt;code&gt;UNION&lt;/code&gt; to save you.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — dialect keyword differences
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Postgres, MySQL 8+, Snowflake, BigQuery, DuckDB&lt;/strong&gt; — the &lt;code&gt;WITH RECURSIVE&lt;/code&gt; keyword is mandatory. Omit it and you get a syntax error at parse time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL Server&lt;/strong&gt; — no &lt;code&gt;RECURSIVE&lt;/code&gt; keyword exists. The parser detects recursion from the self-reference. The rest of the shape (anchor + &lt;code&gt;UNION ALL&lt;/code&gt; + recursive term) is identical.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Oracle&lt;/strong&gt; — supports the standard &lt;code&gt;WITH ... AS (anchor UNION ALL recursive_term)&lt;/code&gt; since 11g Release 2. Legacy code uses &lt;code&gt;CONNECT BY&lt;/code&gt; — do not use in new code, it is not portable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the same running-numbers 1..5 CTE in the four dominant dialects. Highlight the keyword differences.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(none — the CTE generates its own input)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Postgres&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;RECURSIVE&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
    &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- MySQL 8+&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;RECURSIVE&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
    &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- SQL Server (no RECURSIVE keyword)&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
    &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt;
&lt;span class="k"&gt;OPTION&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MAXRECURSION&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;-- optional; default 100&lt;/span&gt;

&lt;span class="c1"&gt;-- Snowflake&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;RECURSIVE&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
    &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- BigQuery&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;RECURSIVE&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
    &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Every dialect ships the same anchor + &lt;code&gt;UNION ALL&lt;/code&gt; + recursive-term shape. The syntactic delta is exactly one keyword (&lt;code&gt;RECURSIVE&lt;/code&gt;) and, for SQL Server, an optional &lt;code&gt;OPTION (MAXRECURSION N)&lt;/code&gt; at the outer query.&lt;/li&gt;
&lt;li&gt;Postgres, MySQL, Snowflake, BigQuery, DuckDB all require &lt;code&gt;RECURSIVE&lt;/code&gt;. Omitting it gives a syntax error, not a silent non-recursive interpretation.&lt;/li&gt;
&lt;li&gt;SQL Server's parser infers recursion when the CTE's &lt;code&gt;FROM&lt;/code&gt; clause references itself. This is a legacy design choice (SQL Server 2005 pre-dated the SQL standard's &lt;code&gt;RECURSIVE&lt;/code&gt; keyword); it works but is easy to miss when porting.&lt;/li&gt;
&lt;li&gt;Oracle 11gR2+ supports the standard shape too, without &lt;code&gt;RECURSIVE&lt;/code&gt; (Oracle also infers). Legacy Oracle used &lt;code&gt;CONNECT BY PRIOR&lt;/code&gt;, which is proprietary and less flexible than the standard CTE — do not use in new code.&lt;/li&gt;
&lt;li&gt;When porting between dialects, adding or removing the &lt;code&gt;RECURSIVE&lt;/code&gt; keyword is usually the only change required. If you also need a max-recursion override, the syntax differs per dialect (see H2-5).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;n&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Write portable code by defaulting to &lt;code&gt;WITH RECURSIVE&lt;/code&gt;. On SQL Server, the keyword is accepted (as of 2019+) but harmless. This keeps you code-compatible across every mainstream engine except very old Oracle.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on recursive CTE anatomy
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Walk me through exactly what the database does when it executes a recursive CTE. What is the anchor doing, what is the recursive term doing, and how does the executor know when to stop? Where can it go wrong?"&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using the anchor + iterative-frontier execution model
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Recursive CTE execution — inside the executor
=============================================

Setup
-----
1. Parser detects the WITH RECURSIVE cte AS (...) shape.
2. The CTE's column list is fixed from the anchor SELECT.
3. Executor allocates two worktables:
   - "accumulated" — every row ever emitted
   - "current" — rows emitted in the last iteration

Anchor (runs once)
------------------
4. Execute the anchor SELECT.
5. Insert rows into BOTH "accumulated" and "current".

Recursive term (iterates)
-------------------------
6. While "current" is not empty:
   a. Execute the recursive term with cte := "current".
   b. Insert emitted rows into "accumulated".
   c. Replace "current" with the emitted rows.
7. Loop terminates when the recursive term emits zero rows.

Outer query
-----------
8. The outer SELECT reads "accumulated" as if it were a regular table.

Failure modes
-------------
9. No termination → max-recursion setting fires → error.
10. Column list mismatch between anchor and recursive term → error.
11. Recursive term references the CTE more than once → error in some dialects.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For running-numbers 1..3 in Postgres:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Iteration&lt;/th&gt;
&lt;th&gt;Current worktable&lt;/th&gt;
&lt;th&gt;Recursive term emits&lt;/th&gt;
&lt;th&gt;Accumulated&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0 (anchor)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{1}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{1}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{1}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{2}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{1, 2}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{2}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{3}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{1, 2, 3}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{3}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;{}&lt;/code&gt; (3 &amp;lt; 3 false)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{1, 2, 3}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;terminate&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{1, 2, 3}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;current&lt;/code&gt; worktable is the &lt;em&gt;incremental&lt;/em&gt; frontier — the last iteration's output, not the whole accumulated set. This is the semantic that lets the executor be O(edges) per step rather than O(accumulated).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Executor concept&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Anchor SELECT&lt;/td&gt;
&lt;td&gt;Runs once; seeds the CTE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Current worktable&lt;/td&gt;
&lt;td&gt;Holds the last iteration's output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accumulated worktable&lt;/td&gt;
&lt;td&gt;Holds every emitted row&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recursive term&lt;/td&gt;
&lt;td&gt;Joins base to current; emits next generation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Termination&lt;/td&gt;
&lt;td&gt;Recursive term returns zero rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Outer SELECT&lt;/td&gt;
&lt;td&gt;Reads accumulated as if it were a regular table&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Anchor + recursive term&lt;/strong&gt;&lt;/strong&gt; — the shape is a standards-defined two-part CTE. The anchor supplies the base case and the column types; the recursive term supplies the induction step. Interviewers listen for this vocabulary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Incremental frontier&lt;/strong&gt;&lt;/strong&gt; — the recursive term joins to the &lt;em&gt;last iteration's output&lt;/em&gt;, not the whole accumulated set. This is why the executor is O(edges × depth) not O(edges × accumulated²).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;&lt;code&gt;UNION ALL&lt;/code&gt; semantics&lt;/strong&gt;&lt;/strong&gt; — every recursive-term row is appended without dedup. Dedup at the CTE level is expensive and rarely correct; use &lt;code&gt;UNION ALL&lt;/code&gt; and dedup in the outer query if needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Termination is your job&lt;/strong&gt;&lt;/strong&gt; — the recursive term must produce zero rows; the executor does not infer termination. Max-recursion settings are a safety net for accidental infinite loops.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;O(anchor + Σ recursive-term over generations)&lt;/code&gt;. For a tree of &lt;code&gt;N&lt;/code&gt; nodes at depth &lt;code&gt;D&lt;/code&gt;, roughly &lt;code&gt;O(N)&lt;/code&gt; since each edge is visited once. For a graph without a cycle guard, &lt;code&gt;O(∞)&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — CTE&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Recursive CTE anatomy problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/cte" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — CTE · hard&lt;/span&gt;
&lt;strong&gt;Hard CTE interview problems&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/cte/difficulty/hard" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  3. Hierarchies and trees — org chart / category tree
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;sql hierarchy query&lt;/code&gt; walks a single-parent tree — anchor selects the root, recursive term joins children, depth column and path array make the tree navigable
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;a hierarchy is a tree where every row has at most one parent; the recursive CTE anchors on the roots (&lt;code&gt;WHERE manager_id IS NULL&lt;/code&gt;), the recursive term joins children of the current frontier (&lt;code&gt;WHERE e.manager_id = t.id&lt;/code&gt;), a &lt;code&gt;level + 1&lt;/code&gt; column tracks depth, and a &lt;code&gt;path&lt;/code&gt; array (or concatenated string) records ancestry&lt;/strong&gt;. Once you say that out loud, every &lt;code&gt;sql hierarchy query&lt;/code&gt; interview question about org charts, category trees, comment threads, and filesystems becomes the same skeleton with the label swapped.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzyl5vuk9tux1xeibdqkz.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzyl5vuk9tux1xeibdqkz.jpeg" alt="Visual diagram of an org-chart tree traversal — left a 4-level employee tree with CEO at the top and levels 1..3 below; right a running result card showing employee_id, name, level, path columns as they get produced by successive recursive steps; on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The canonical shape.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Anchor.&lt;/strong&gt; Selects the roots — rows where the parent column is NULL, or a specific starting row when you want a sub-tree.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recursive term.&lt;/strong&gt; Joins the base table to the CTE where &lt;code&gt;base.parent_id = cte.id&lt;/code&gt;. Emits the direct reports of every current-frontier row.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Level column.&lt;/strong&gt; &lt;code&gt;cte.level + 1&lt;/code&gt; on every recursive step. Root is level 0 (or 1 — pick one and stick to it).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Path array.&lt;/strong&gt; &lt;code&gt;cte.path || base.id&lt;/code&gt; on every recursive step. Records the full ancestry chain from root to this row.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Termination.&lt;/strong&gt; Automatic — leaves have no children; the recursive term drains.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Two-column identity: &lt;code&gt;(level, path)&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Combined, &lt;code&gt;(level, path)&lt;/code&gt; uniquely identifies each row's position in the tree.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ORDER BY path&lt;/code&gt; renders the tree in depth-first order (siblings appear together, then their children, then the next sibling).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ORDER BY level, name&lt;/code&gt; renders the tree in breadth-first level order (all level-0, then all level-1, then all level-2).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Path array vs concatenated string.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Postgres.&lt;/strong&gt; Native array support: &lt;code&gt;ARRAY[id]&lt;/code&gt;, &lt;code&gt;path || e.id&lt;/code&gt;. Comparisons are natural (&lt;code&gt;= ANY&lt;/code&gt;, &lt;code&gt;&amp;lt;@&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MySQL.&lt;/strong&gt; &lt;code&gt;CONCAT(cte.path, '/', e.id)&lt;/code&gt; returns a string. Watch for lexicographic sort order (&lt;code&gt;'2' &amp;gt; '10'&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL Server / Snowflake / BigQuery.&lt;/strong&gt; Use &lt;code&gt;CONCAT&lt;/code&gt; or string helpers; padded integers (&lt;code&gt;FORMAT(id, '000000')&lt;/code&gt;) keep sort order sane.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Where the anchor changes shape.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Whole tree.&lt;/strong&gt; &lt;code&gt;WHERE parent_id IS NULL&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Descendants of a specific row.&lt;/strong&gt; &lt;code&gt;WHERE id = :root_id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ancestors of a specific row.&lt;/strong&gt; Flip the join direction — anchor is &lt;code&gt;WHERE id = :leaf_id&lt;/code&gt;, recursive term is &lt;code&gt;JOIN employees e ON cte.manager_id = e.id&lt;/code&gt;. Walks up instead of down.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Depth-first vs breadth-first ordering.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Depth-first (DFS).&lt;/strong&gt; Emit each sub-tree in full before moving to the next sibling. &lt;code&gt;ORDER BY path&lt;/code&gt; on a tuple-encoded or padded-string path produces DFS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Breadth-first (BFS).&lt;/strong&gt; Emit all level-0, then level-1, then level-2. &lt;code&gt;ORDER BY level, name&lt;/code&gt; produces BFS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres shortcut.&lt;/strong&gt; &lt;code&gt;SEARCH DEPTH FIRST BY id SET seq&lt;/code&gt; or &lt;code&gt;SEARCH BREADTH FIRST BY id SET seq&lt;/code&gt; adds a &lt;code&gt;seq&lt;/code&gt; column that sorts correctly without hand-coding the path.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Category tree — the e-commerce twin.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Products belong to categories that belong to categories: &lt;code&gt;Electronics → Computers → Laptops → Gaming Laptops&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Same shape as the org chart, just different labels. Anchor is &lt;code&gt;WHERE parent_category_id IS NULL&lt;/code&gt;; recursive term joins children.&lt;/li&gt;
&lt;li&gt;Common interview probe: "given a category id, list every product in that category &lt;em&gt;or any descendant category&lt;/em&gt;." Solved with a recursive CTE on categories plus a join to products.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on hierarchies.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"How do you list every employee under a specific manager?" — recursive CTE with the manager as the anchor, recursive term joins direct reports.&lt;/li&gt;
&lt;li&gt;"How do you compute the depth of every node in an org chart?" — recursive CTE anchored on &lt;code&gt;manager_id IS NULL&lt;/code&gt;, with &lt;code&gt;level + 1&lt;/code&gt; per recursive step.&lt;/li&gt;
&lt;li&gt;"How do you find the CEO of an arbitrary employee?" — flip the direction — anchor on that employee, recursive term walks up.&lt;/li&gt;
&lt;li&gt;"How do you sort the tree depth-first?" — carry a path array (or padded-string) and &lt;code&gt;ORDER BY path&lt;/code&gt; in the outer query.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — employees under a specific manager
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The interview probe.&lt;/strong&gt; "Given &lt;code&gt;manager_id = 2&lt;/code&gt; (Bob), list every employee under Bob with their depth and full path from Bob." This is the "subtree query" — the anchor is a specific row rather than the whole root set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The delta from the whole-tree walk.&lt;/strong&gt; Only the anchor changes. Instead of &lt;code&gt;WHERE manager_id IS NULL&lt;/code&gt;, it becomes &lt;code&gt;WHERE id = 2&lt;/code&gt;. The recursive term is identical.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What the path represents.&lt;/strong&gt; &lt;code&gt;path = [2, 4, 7]&lt;/code&gt; means Grace's chain from Bob (the query root) is &lt;code&gt;Bob → Dan → Grace&lt;/code&gt;. Note that this path starts at Bob, not at the tree root (Alice) — it is a subtree path.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write a recursive CTE that returns every employee under &lt;code&gt;manager_id = 2&lt;/code&gt; (Bob), with &lt;code&gt;level&lt;/code&gt; (Bob = 0) and &lt;code&gt;path&lt;/code&gt; (starts at Bob).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input — employees.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;manager_id&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Alice&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Bob&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Carol&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Dan&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Eve&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Frank&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Grace&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;RECURSIVE&lt;/span&gt; &lt;span class="n"&gt;subtree&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="c1"&gt;-- anchor: Bob himself&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;manager_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;level&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ARRAY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;

    &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;

    &lt;span class="c1"&gt;-- recursive: children of the frontier&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;manager_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;level&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
    &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;subtree&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;manager_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;level&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;subtree&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Anchor.&lt;/strong&gt; Selects Bob (id=2). Emits &lt;code&gt;(2, Bob, 1, 0, [2])&lt;/code&gt;. Frontier is &lt;code&gt;{Bob}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recursive step 1.&lt;/strong&gt; Joins children of Bob. Emits Dan (&lt;code&gt;level=1&lt;/code&gt;, &lt;code&gt;path=[2, 4]&lt;/code&gt;) and Eve (&lt;code&gt;level=1&lt;/code&gt;, &lt;code&gt;path=[2, 5]&lt;/code&gt;). Frontier now &lt;code&gt;{Dan, Eve}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recursive step 2.&lt;/strong&gt; Children of Dan: Grace (&lt;code&gt;level=2&lt;/code&gt;, &lt;code&gt;path=[2, 4, 7]&lt;/code&gt;). Children of Eve: none. Frontier now &lt;code&gt;{Grace}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recursive step 3.&lt;/strong&gt; Children of Grace: none. Recursive term emits zero rows → CTE terminates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outer query.&lt;/strong&gt; &lt;code&gt;ORDER BY path&lt;/code&gt; gives depth-first traversal from Bob: &lt;code&gt;Bob → Dan → Grace → Eve&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;level&lt;/th&gt;
&lt;th&gt;path&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Bob&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;[2]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Dan&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;[2, 4]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Grace&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;[2, 4, 7]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Eve&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;[2, 5]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For subtree queries, change only the anchor. The recursive term stays identical across "whole tree," "subtree under X," and "ancestors of X" (though for ancestors, the join direction flips).&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — ancestors of an employee
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The direction flip.&lt;/strong&gt; For descendants, the recursive term joins &lt;code&gt;e.manager_id = cte.id&lt;/code&gt;. For ancestors, it joins &lt;code&gt;cte.manager_id = e.id&lt;/code&gt; — cte holds the child, e is the parent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The termination.&lt;/strong&gt; Automatic. The root has &lt;code&gt;manager_id IS NULL&lt;/code&gt;; the recursive term joining a NULL manager_id emits zero rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The path.&lt;/strong&gt; Prepends parents as you walk up: &lt;code&gt;[7, 4, 2, 1]&lt;/code&gt; for Grace's ancestry to Alice. &lt;code&gt;path[1]&lt;/code&gt; is Grace, &lt;code&gt;path[-1]&lt;/code&gt; (or reversed) is the CEO.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given &lt;code&gt;employee_id = 7&lt;/code&gt; (Grace), return every ancestor in the chain up to the CEO, with depth (Grace = 0, CEO = 3 in this tree).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input — same employees table as above.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;RECURSIVE&lt;/span&gt; &lt;span class="n"&gt;ancestors&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="c1"&gt;-- anchor: Grace herself&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;manager_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;depth_up&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;

    &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;

    &lt;span class="c1"&gt;-- recursive: walk UP via manager_id&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;manager_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;depth_up&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
    &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;ancestors&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;manager_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;depth_up&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ancestors&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;depth_up&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Anchor.&lt;/strong&gt; Selects Grace (id=7). Frontier &lt;code&gt;{Grace}&lt;/code&gt;, &lt;code&gt;depth_up = 0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recursive step 1.&lt;/strong&gt; Joins Grace's manager: Dan (id=4). Emits &lt;code&gt;(4, Dan, 2, 1)&lt;/code&gt;. Frontier &lt;code&gt;{Dan}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recursive step 2.&lt;/strong&gt; Joins Dan's manager: Bob (id=2). Emits &lt;code&gt;(2, Bob, 1, 2)&lt;/code&gt;. Frontier &lt;code&gt;{Bob}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recursive step 3.&lt;/strong&gt; Joins Bob's manager: Alice (id=1). Emits &lt;code&gt;(1, Alice, NULL, 3)&lt;/code&gt;. Frontier &lt;code&gt;{Alice}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recursive step 4.&lt;/strong&gt; Alice's &lt;code&gt;manager_id IS NULL&lt;/code&gt;. The recursive term's join &lt;code&gt;ON a.manager_id = e.id&lt;/code&gt; has NULL on the left side → no match → zero rows emitted → CTE terminates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outer query.&lt;/strong&gt; Orders by &lt;code&gt;depth_up&lt;/code&gt; — starts at Grace (0), ends at the CEO Alice (3).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;depth_up&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Grace&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Dan&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Bob&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Alice&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; The direction of the recursive term's join determines "up" vs "down." Descendants use &lt;code&gt;e.manager_id = cte.id&lt;/code&gt;; ancestors use &lt;code&gt;cte.manager_id = e.id&lt;/code&gt;. Everything else stays the same.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — category tree with product rollup
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The e-commerce twin.&lt;/strong&gt; A &lt;code&gt;categories&lt;/code&gt; table has &lt;code&gt;(id, name, parent_category_id)&lt;/code&gt;. A &lt;code&gt;products&lt;/code&gt; table has &lt;code&gt;(id, name, category_id)&lt;/code&gt;. Interview probe: "list every product in category 3 &lt;em&gt;or any descendant category of 3&lt;/em&gt;."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The two-CTE chain.&lt;/strong&gt; First recursive CTE walks the category tree from &lt;code&gt;id = 3&lt;/code&gt;. Second (regular) CTE joins products to that walk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why not a single query.&lt;/strong&gt; You &lt;em&gt;can&lt;/em&gt; inline the join, but the two-step form is more readable and often easier for the optimiser to plan.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given &lt;code&gt;category_id = 3&lt;/code&gt; (Computers), return every product in that category or any descendant. Show the category tree walk and the product join.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input — categories.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;parent_category_id&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Everything&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Books&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Computers&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Laptops&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Gaming&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Desktops&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Input — products.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;category_id&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;Novel&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;Textbook&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;td&gt;MacBook&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;td&gt;Alienware&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;iMac&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;RECURSIVE&lt;/span&gt; &lt;span class="n"&gt;cat_tree&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="c1"&gt;-- anchor: Computers&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parent_category_id&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;categories&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;

    &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;

    &lt;span class="c1"&gt;-- recursive: descendants&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent_category_id&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;categories&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
    &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;cat_tree&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent_category_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;cat_tree&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Anchor.&lt;/strong&gt; Selects &lt;code&gt;Computers&lt;/code&gt; (id=3). Frontier &lt;code&gt;{Computers}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recursive step 1.&lt;/strong&gt; Children of Computers: Laptops (id=4), Desktops (id=6). Frontier &lt;code&gt;{Laptops, Desktops}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recursive step 2.&lt;/strong&gt; Children of Laptops: Gaming (id=5). Children of Desktops: none. Frontier &lt;code&gt;{Gaming}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recursive step 3.&lt;/strong&gt; Children of Gaming: none. Recursive term emits zero rows → CTE terminates. &lt;code&gt;cat_tree&lt;/code&gt; now contains &lt;code&gt;{Computers, Laptops, Desktops, Gaming}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outer query.&lt;/strong&gt; Joins &lt;code&gt;products&lt;/code&gt; to &lt;code&gt;cat_tree&lt;/code&gt;. Every product whose &lt;code&gt;category_id&lt;/code&gt; matches any row in the tree passes through. MacBook (Laptops), Alienware (Gaming), iMac (Desktops) match; Novel and Textbook (both Books) do not.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;category&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;td&gt;MacBook&lt;/td&gt;
&lt;td&gt;Laptops&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;td&gt;Alienware&lt;/td&gt;
&lt;td&gt;Gaming&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;iMac&lt;/td&gt;
&lt;td&gt;Desktops&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For "products in a category or any descendant category," write the recursive CTE on the category tree first, then join products. The two-step shape reads naturally and gives the planner clear boundaries to reason about.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on org-chart traversal
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Write a SQL query that returns, for every employee, their name, their level in the org chart (CEO = 0), their manager's name, and the total number of employees under them (direct + indirect reports). One query, no application code."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a recursive CTE plus an aggregate join
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;RECURSIVE&lt;/span&gt; &lt;span class="n"&gt;org&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="c1"&gt;-- anchor: the CEO&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;manager_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;level&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ARRAY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;manager_id&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;

    &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;

    &lt;span class="c1"&gt;-- recursive: children&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;manager_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;level&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
    &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;org&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;manager_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;reports_count&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="c1"&gt;-- for every row, count how many other rows have this row's id in their path&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;emp_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_reports&lt;/span&gt;   &lt;span class="c1"&gt;-- minus self&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;org&lt;/span&gt; &lt;span class="n"&gt;o1&lt;/span&gt;
    &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;org&lt;/span&gt; &lt;span class="n"&gt;o2&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;o1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;ANY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;o1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;employee&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;level&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_reports&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;org&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;manager_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;reports_count&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;emp_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Using the seven-row employees table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Anchor: emit &lt;code&gt;(Alice, 0, [1])&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Recursive: children of Alice → Bob (level 1, &lt;code&gt;[1,2]&lt;/code&gt;), Carol (level 1, &lt;code&gt;[1,3]&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Recursive: children of Bob, Carol → Dan &lt;code&gt;[1,2,4]&lt;/code&gt;, Eve &lt;code&gt;[1,2,5]&lt;/code&gt;, Frank &lt;code&gt;[1,3,6]&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Recursive: children of Dan/Eve/Frank → Grace &lt;code&gt;[1,2,4,7]&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Recursive: zero rows → CTE &lt;code&gt;org&lt;/code&gt; terminates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;reports_count&lt;/code&gt; CTE joins &lt;code&gt;org&lt;/code&gt; to itself where &lt;code&gt;o1.id = ANY(o2.path)&lt;/code&gt; — for every ancestor, count descendants&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Outer SELECT joins &lt;code&gt;org&lt;/code&gt; to &lt;code&gt;employees&lt;/code&gt; (via manager_id) and to &lt;code&gt;reports_count&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ORDER BY path&lt;/code&gt; renders DFS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The self-join in &lt;code&gt;reports_count&lt;/code&gt; uses &lt;code&gt;o1.id = ANY(o2.path)&lt;/code&gt;: row &lt;code&gt;o1&lt;/code&gt; is "in the path of" row &lt;code&gt;o2&lt;/code&gt; if and only if &lt;code&gt;o1&lt;/code&gt; is an ancestor of &lt;code&gt;o2&lt;/code&gt;. Grouping by &lt;code&gt;o1.id&lt;/code&gt; and counting &lt;code&gt;o2.id&lt;/code&gt; (minus 1 for self) gives the count of descendants.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;employee&lt;/th&gt;
&lt;th&gt;level&lt;/th&gt;
&lt;th&gt;manager&lt;/th&gt;
&lt;th&gt;total_reports&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Alice&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;(NULL)&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bob&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Alice&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dan&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Bob&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grace&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Dan&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eve&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Bob&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Carol&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Alice&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frank&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Carol&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Path array as ancestry chain&lt;/strong&gt;&lt;/strong&gt; — carrying the full path from root to this row makes every ancestor addressable. &lt;code&gt;o1.id = ANY(o2.path)&lt;/code&gt; is a set-membership test that returns TRUE if and only if &lt;code&gt;o1&lt;/code&gt; is an ancestor of &lt;code&gt;o2&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Self-join on org&lt;/strong&gt;&lt;/strong&gt; — the CTE is used twice in the same query: once to enumerate rows, once to count descendants via path membership. A regular CTE is fine here because the second usage does not itself recurse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;LEFT JOIN for the manager name&lt;/strong&gt;&lt;/strong&gt; — the CEO's &lt;code&gt;manager_id&lt;/code&gt; is NULL; a LEFT JOIN preserves the row with a NULL manager name rather than dropping it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;ORDER BY path&lt;/strong&gt;&lt;/strong&gt; — arrays compare element-wise; &lt;code&gt;[1] &amp;lt; [1,2] &amp;lt; [1,2,4] &amp;lt; [1,2,4,7] &amp;lt; [1,2,5] &amp;lt; [1,3] &amp;lt; [1,3,6]&lt;/code&gt;. That is exactly the DFS pre-order.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — recursive CTE is O(N) for a tree of N nodes. The &lt;code&gt;reports_count&lt;/code&gt; self-join is O(N × avg_depth); on org charts with depth ≤ 10 and N ≤ 100K, that's a millisecond query.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — tree&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Tree traversal problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/tree" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — CTEs&lt;/span&gt;
&lt;strong&gt;CTE hierarchy problems&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/ctes" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  4. Graph traversal and cycle detection
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;graph traversal sql&lt;/code&gt; walks a multi-parent (possibly cyclic) edge table — the path array is your memory, the &lt;code&gt;NOT visited&lt;/code&gt; predicate is your cycle guard, and Postgres has built-in &lt;code&gt;SEARCH&lt;/code&gt; and &lt;code&gt;CYCLE&lt;/code&gt; clauses
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;a graph is an edges table &lt;code&gt;(src, dst)&lt;/code&gt; where any node can point to many; the recursive CTE anchors on the starting node, the recursive term joins every incident edge to the frontier, an accumulated &lt;code&gt;path&lt;/code&gt; array records visited nodes, and a &lt;code&gt;NOT node = ANY(path)&lt;/code&gt; predicate blocks cycles&lt;/strong&gt;. Once you say that out loud, every friendship-BFS, permission-chain, or dependency-graph interview question becomes the same recipe with different labels.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwt5s3c2a1c4v35k4rp40.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwt5s3c2a1c4v35k4rp40.jpeg" alt="Visual diagram of graph traversal with cycle detection — left a 6-node friendship graph with a highlighted cycle A→B→C→A; middle a BFS frontier card showing successive hops with visited-path arrays; right a red-outlined 'cycle guard' card showing the NOT node = ANY(path) predicate and Postgres CYCLE clause; on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Directed vs undirected edges.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Directed.&lt;/strong&gt; &lt;code&gt;edges(src, dst)&lt;/code&gt; means &lt;code&gt;src → dst&lt;/code&gt; only. Recursive term joins &lt;code&gt;edges.src = cte.node_id&lt;/code&gt;; emits &lt;code&gt;edges.dst&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Undirected.&lt;/strong&gt; Same table, but &lt;code&gt;src ↔ dst&lt;/code&gt; (both ways). Recursive term joins where &lt;code&gt;edges.src = cte.node_id&lt;/code&gt; &lt;em&gt;or&lt;/em&gt; &lt;code&gt;edges.dst = cte.node_id&lt;/code&gt;; emits the &lt;em&gt;other&lt;/em&gt; endpoint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Symmetric edge trick.&lt;/strong&gt; Store undirected edges as two rows (&lt;code&gt;(a, b)&lt;/code&gt; and &lt;code&gt;(b, a)&lt;/code&gt;) and treat as directed. Trades storage for query simplicity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;BFS via recursive CTE.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Anchor emits the starting node with &lt;code&gt;hop = 0&lt;/code&gt;, &lt;code&gt;path = [start]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Recursive term joins each frontier node to &lt;code&gt;edges&lt;/code&gt;, filtered by &lt;code&gt;NOT dst = ANY(path)&lt;/code&gt;. Increments &lt;code&gt;hop&lt;/code&gt;, appends &lt;code&gt;dst&lt;/code&gt; to &lt;code&gt;path&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Terminates when every reachable-from-start node has been visited (frontier drains).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cycle detection — three flavours.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Path array with &lt;code&gt;= ANY&lt;/code&gt;&lt;/strong&gt; (Postgres, DuckDB) — &lt;code&gt;WHERE NOT child = ANY(cte.path)&lt;/code&gt;. O(depth) per row, but O(depth) is small.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concatenated string with &lt;code&gt;LIKE&lt;/code&gt;&lt;/strong&gt; (MySQL, SQL Server) — &lt;code&gt;WHERE cte.path NOT LIKE CONCAT('%,', child, ',%')&lt;/code&gt;. Same idea; slower on very deep paths.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres &lt;code&gt;CYCLE&lt;/code&gt; clause&lt;/strong&gt; — declarative: &lt;code&gt;CYCLE id SET is_cycle USING path&lt;/code&gt;. Adds &lt;code&gt;is_cycle&lt;/code&gt; and &lt;code&gt;path&lt;/code&gt; columns automatically; you filter &lt;code&gt;WHERE NOT is_cycle&lt;/code&gt; in the outer query.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Postgres &lt;code&gt;SEARCH&lt;/code&gt; and &lt;code&gt;CYCLE&lt;/code&gt; — the built-ins.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SEARCH BREADTH FIRST BY col SET seq&lt;/code&gt;&lt;/strong&gt; — adds a &lt;code&gt;seq&lt;/code&gt; column that, when you &lt;code&gt;ORDER BY seq&lt;/code&gt;, gives BFS order.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SEARCH DEPTH FIRST BY col SET seq&lt;/code&gt;&lt;/strong&gt; — same, but DFS order.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;CYCLE col SET is_cycle USING path&lt;/code&gt;&lt;/strong&gt; — adds &lt;code&gt;is_cycle&lt;/code&gt; (bool) and &lt;code&gt;path&lt;/code&gt; (array) columns. Sets &lt;code&gt;is_cycle = true&lt;/code&gt; the moment a cycle is detected; the recursive term stops on cycle-flagged rows. Combines cleanly with &lt;code&gt;SEARCH&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Shortest path — recursive CTE + aggregation.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Recursive CTE walks all paths from a start.&lt;/li&gt;
&lt;li&gt;Outer query: &lt;code&gt;SELECT MIN(hop) FROM cte WHERE node_id = :target&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Not efficient for large graphs (recursive term enumerates every path), but correct.&lt;/li&gt;
&lt;li&gt;For large graphs, use a graph DB or an app-side BFS with proper visited state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on graphs.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"How do you detect a cycle in a graph in SQL?" — accumulate a &lt;code&gt;path&lt;/code&gt; array in the recursive CTE; filter &lt;code&gt;NOT child = ANY(path)&lt;/code&gt; on the recursive term.&lt;/li&gt;
&lt;li&gt;"How do you find the shortest path between two nodes?" — recursive CTE that walks all paths; &lt;code&gt;MIN(hop) FROM cte WHERE node_id = :target&lt;/code&gt;. Slow but correct.&lt;/li&gt;
&lt;li&gt;"What does Postgres' &lt;code&gt;SEARCH BREADTH FIRST&lt;/code&gt; clause do?" — adds an ordering column that sorts BFS-first without hand-coding path arithmetic.&lt;/li&gt;
&lt;li&gt;"When would you not use a recursive CTE for a graph?" — high-QPS shortest-path queries on large graphs → graph DB.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — undirected friendship graph BFS
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The setup.&lt;/strong&gt; A &lt;code&gt;friendships(a, b)&lt;/code&gt; table storing undirected edges. To make the join symmetric, we treat both &lt;code&gt;friendships.a = cte.node_id&lt;/code&gt; and &lt;code&gt;friendships.b = cte.node_id&lt;/code&gt; as matches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The visited-path guard.&lt;/strong&gt; Without a guard, the recursive term keeps re-visiting nodes and looping through the triangle &lt;code&gt;1-2-3&lt;/code&gt; forever. &lt;code&gt;WHERE NOT other_endpoint = ANY(path)&lt;/code&gt; stops it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The output shape.&lt;/strong&gt; For each starting node, we get one row per reachable node with &lt;code&gt;hop&lt;/code&gt; (distance) and &lt;code&gt;path&lt;/code&gt; (the sequence).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given an undirected friendship graph, write a recursive CTE that returns every friend-of-a-friend (up to 3 hops) from user 1, with the hop distance and the full path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input — friendships (undirected).&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;a&lt;/th&gt;
&lt;th&gt;b&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;RECURSIVE&lt;/span&gt; &lt;span class="n"&gt;bfs&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="c1"&gt;-- anchor: user 1&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;node_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;hop&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ARRAY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;
    &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;node_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hop&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;bfs&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
    &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="k"&gt;LATERAL&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node_id&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;friendships&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
        &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node_id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;ANY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hop&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;node_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hop&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;bfs&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;hop&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;node_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Anchor.&lt;/strong&gt; Emits user 1 with &lt;code&gt;hop = 0&lt;/code&gt;, &lt;code&gt;path = [1]&lt;/code&gt;. Frontier &lt;code&gt;{1}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recursive step 1.&lt;/strong&gt; For frontier node 1, joins friendships where &lt;code&gt;1 IN (a, b)&lt;/code&gt;. Matches edges &lt;code&gt;(1,2)&lt;/code&gt; and &lt;code&gt;(1,3)&lt;/code&gt;. The &lt;code&gt;LATERAL&lt;/code&gt; picks the &lt;em&gt;other&lt;/em&gt; endpoint. Filter &lt;code&gt;NOT other = ANY(path)&lt;/code&gt; is true for both. Filter &lt;code&gt;hop &amp;lt; 3&lt;/code&gt; is true. Emits nodes 2 and 3 with &lt;code&gt;hop = 1&lt;/code&gt;. Paths &lt;code&gt;[1, 2]&lt;/code&gt; and &lt;code&gt;[1, 3]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recursive step 2.&lt;/strong&gt; Frontier is &lt;code&gt;{2, 3}&lt;/code&gt;. From node 2: edges &lt;code&gt;(1,2)&lt;/code&gt; and &lt;code&gt;(2,4)&lt;/code&gt;. Other endpoints: 1 (blocked by path guard), 4 (allowed). From node 3: edges &lt;code&gt;(1,3)&lt;/code&gt; and &lt;code&gt;(3,4)&lt;/code&gt;. Other endpoints: 1 (blocked), 4 (allowed via different path &lt;code&gt;[1, 3, 4]&lt;/code&gt;). Emits &lt;code&gt;(4, 2, [1, 2, 4])&lt;/code&gt; and &lt;code&gt;(4, 2, [1, 3, 4])&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recursive step 3.&lt;/strong&gt; Frontier is &lt;code&gt;{4 (from path [1,2,4]), 4 (from path [1,3,4])}&lt;/code&gt;. From node 4: edges &lt;code&gt;(2,4)&lt;/code&gt;, &lt;code&gt;(3,4)&lt;/code&gt;, &lt;code&gt;(4,5)&lt;/code&gt;. Other endpoints: 2, 3, 5. Path guards block 2 and 3 (already in each path). 5 is allowed. Emits &lt;code&gt;(5, 3, [1, 2, 4, 5])&lt;/code&gt; and &lt;code&gt;(5, 3, [1, 3, 4, 5])&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recursive step 4.&lt;/strong&gt; Frontier is &lt;code&gt;{5, 5}&lt;/code&gt;. But &lt;code&gt;hop &amp;lt; 3&lt;/code&gt; is false (would be 4) → recursive term emits zero rows → CTE terminates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outer query.&lt;/strong&gt; Orders by &lt;code&gt;(hop, node_id)&lt;/code&gt;. Returns 1 at hop 0, 2/3 at hop 1, 4 at hop 2 (two paths), 5 at hop 3 (two paths).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;node_id&lt;/th&gt;
&lt;th&gt;hop&lt;/th&gt;
&lt;th&gt;path&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;[1]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;[1, 2]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;[1, 3]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;[1, 2, 4]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;[1, 3, 4]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;[1, 2, 4, 5]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;[1, 3, 4, 5]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; BFS in SQL emits every reachable path, not every unique node. For "unique nodes at each hop," dedup with &lt;code&gt;SELECT DISTINCT node_id, MIN(hop)&lt;/code&gt; in the outer query. Two paths to the same node at the same hop is a signal of shortest-path ties.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — cycle detection with a directed graph
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The setup.&lt;/strong&gt; A directed &lt;code&gt;edges(src, dst)&lt;/code&gt; table containing a cycle &lt;code&gt;1 → 2 → 3 → 1&lt;/code&gt;. Without a cycle guard, the recursive term loops forever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The path predicate.&lt;/strong&gt; &lt;code&gt;WHERE NOT dst = ANY(path)&lt;/code&gt; prevents re-visiting. When &lt;code&gt;dst = 1&lt;/code&gt; and &lt;code&gt;path = [1, 2, 3]&lt;/code&gt;, the guard blocks the emission.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Detecting the cycle explicitly.&lt;/strong&gt; If the interviewer wants "list the cycles," track the cycle with an additional flag: &lt;code&gt;is_cycle := dst = ANY(path)&lt;/code&gt; on the emit; then &lt;code&gt;WHERE is_cycle&lt;/code&gt; in the outer query.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given a directed graph with a cycle &lt;code&gt;1 → 2 → 3 → 1&lt;/code&gt;, walk from node 1 and detect the cycle. Return the path that closes the loop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input — edges (directed).&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;src&lt;/th&gt;
&lt;th&gt;dst&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;RECURSIVE&lt;/span&gt; &lt;span class="n"&gt;walk&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;node_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ARRAY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;is_cycle&lt;/span&gt;
    &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dst&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dst&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dst&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;ANY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;is_cycle&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;walk&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;
    &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;edges&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_cycle&lt;/span&gt;                       &lt;span class="c1"&gt;-- stop expanding once cycle found on this path&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;node_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_cycle&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;walk&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;array_length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Anchor.&lt;/strong&gt; &lt;code&gt;(1, [1], false)&lt;/code&gt;. Frontier &lt;code&gt;{1}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recursive step 1.&lt;/strong&gt; Edge &lt;code&gt;1 → 2&lt;/code&gt;. &lt;code&gt;dst = 2&lt;/code&gt; not in &lt;code&gt;[1]&lt;/code&gt; → &lt;code&gt;is_cycle = false&lt;/code&gt;. Emits &lt;code&gt;(2, [1, 2], false)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recursive step 2.&lt;/strong&gt; Edge &lt;code&gt;2 → 3&lt;/code&gt;. &lt;code&gt;dst = 3&lt;/code&gt; not in &lt;code&gt;[1, 2]&lt;/code&gt; → &lt;code&gt;is_cycle = false&lt;/code&gt;. Emits &lt;code&gt;(3, [1, 2, 3], false)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recursive step 3.&lt;/strong&gt; From node 3: edges &lt;code&gt;3 → 1&lt;/code&gt; and &lt;code&gt;3 → 4&lt;/code&gt;.

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;3 → 1&lt;/code&gt;: &lt;code&gt;dst = 1&lt;/code&gt; &lt;em&gt;is&lt;/em&gt; in &lt;code&gt;[1, 2, 3]&lt;/code&gt; → &lt;code&gt;is_cycle = true&lt;/code&gt;. Emits &lt;code&gt;(1, [1, 2, 3, 1], true)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;3 → 4&lt;/code&gt;: &lt;code&gt;dst = 4&lt;/code&gt; not in &lt;code&gt;[1, 2, 3]&lt;/code&gt; → &lt;code&gt;is_cycle = false&lt;/code&gt;. Emits &lt;code&gt;(4, [1, 2, 3, 4], false)&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recursive step 4.&lt;/strong&gt; Frontier row &lt;code&gt;(1, [1, 2, 3, 1], true)&lt;/code&gt; is blocked from expanding by &lt;code&gt;WHERE NOT w.is_cycle&lt;/code&gt;. Frontier row &lt;code&gt;(4, [1, 2, 3, 4], false)&lt;/code&gt;: no outgoing edges from 4 → emits nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recursive step 5.&lt;/strong&gt; Zero rows → CTE terminates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outer query.&lt;/strong&gt; &lt;code&gt;WHERE is_cycle&lt;/code&gt; returns exactly the row(s) that closed a cycle.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;node_id&lt;/th&gt;
&lt;th&gt;path&lt;/th&gt;
&lt;th&gt;is_cycle&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;[1]&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;[1, 2]&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;[1, 2, 3]&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;[1, 2, 3, 1]&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;[1, 2, 3, 4]&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For every graph traversal, add a path column and a cycle guard even if you "know" the data is cycle-free. The overhead is O(depth) per row — negligible — and the safety is invaluable. For explicit cycle reporting, add an &lt;code&gt;is_cycle&lt;/code&gt; boolean and stop expanding on true.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — Postgres &lt;code&gt;SEARCH&lt;/code&gt; + &lt;code&gt;CYCLE&lt;/code&gt; built-ins
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What &lt;code&gt;SEARCH&lt;/code&gt; does.&lt;/strong&gt; Adds an ordering column that, when you &lt;code&gt;ORDER BY&lt;/code&gt; it, gives DFS or BFS order without hand-coding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What &lt;code&gt;CYCLE&lt;/code&gt; does.&lt;/strong&gt; Adds &lt;code&gt;is_cycle&lt;/code&gt; (bool) and &lt;code&gt;path&lt;/code&gt; (array) columns automatically. Sets &lt;code&gt;is_cycle = true&lt;/code&gt; the moment a cycle is detected; the recursive term stops expanding cycle-flagged rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why prefer them.&lt;/strong&gt; Declarative, less error-prone, harder to introduce a bug. And often the planner can optimise a &lt;code&gt;CYCLE&lt;/code&gt;-clause CTE more aggressively than a hand-coded path predicate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Rewrite the cycle-detection query using Postgres' &lt;code&gt;SEARCH DEPTH FIRST&lt;/code&gt; and &lt;code&gt;CYCLE&lt;/code&gt; clauses. Compare readability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input — same directed edges table with a &lt;code&gt;1 → 2 → 3 → 1&lt;/code&gt; cycle.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;RECURSIVE&lt;/span&gt; &lt;span class="n"&gt;walk&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dst&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;walk&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;
    &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;edges&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SEARCH&lt;/span&gt; &lt;span class="n"&gt;DEPTH&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;node_id&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;seq&lt;/span&gt;
&lt;span class="k"&gt;CYCLE&lt;/span&gt; &lt;span class="n"&gt;node_id&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;is_cycle&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;node_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_cycle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seq&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;walk&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Postgres synthesises the &lt;code&gt;path&lt;/code&gt; column automatically because of the &lt;code&gt;CYCLE ... USING path&lt;/code&gt; clause. No manual &lt;code&gt;ARRAY[node_id]&lt;/code&gt; in the anchor.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;CYCLE&lt;/code&gt; clause tracks visited nodes for us. When the recursive term is about to emit a row whose &lt;code&gt;node_id&lt;/code&gt; is already in &lt;code&gt;path&lt;/code&gt;, Postgres marks &lt;code&gt;is_cycle = true&lt;/code&gt; and does not expand that row further.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;SEARCH DEPTH FIRST BY node_id SET seq&lt;/code&gt; adds a &lt;code&gt;seq&lt;/code&gt; integer column. Rows sorted by &lt;code&gt;seq&lt;/code&gt; are in DFS pre-order.&lt;/li&gt;
&lt;li&gt;The outer query orders by &lt;code&gt;seq&lt;/code&gt;. Postgres has already done the visited-set bookkeeping.&lt;/li&gt;
&lt;li&gt;Compared to the hand-coded version: no explicit path array, no explicit &lt;code&gt;NOT ... = ANY(path)&lt;/code&gt; predicate, no explicit &lt;code&gt;is_cycle&lt;/code&gt; boolean. Postgres does all three declaratively.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;node_id&lt;/th&gt;
&lt;th&gt;path&lt;/th&gt;
&lt;th&gt;is_cycle&lt;/th&gt;
&lt;th&gt;seq&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;{1}&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;{1, 2}&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;{1, 2, 3}&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;{1, 2, 3, 1}&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;{1, 2, 3, 4}&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; On Postgres 14+, prefer &lt;code&gt;SEARCH&lt;/code&gt; and &lt;code&gt;CYCLE&lt;/code&gt; for any graph traversal — cleaner code, fewer bugs, and often a better plan. On other dialects, hand-code the path array and predicate. The mental model is identical.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on graph traversal
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Given a directed graph edges table, write a recursive CTE that finds the shortest path between two specific nodes, protects against cycles, and returns the path as an array. Walk me through how it works and where it can go wrong."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a path-tracking recursive CTE + MIN aggregation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;RECURSIVE&lt;/span&gt; &lt;span class="n"&gt;paths&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;start_id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;node_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ARRAY&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;start_id&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;hop&lt;/span&gt;
    &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dst&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dst&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hop&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;paths&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
    &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;edges&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dst&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;ANY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;              &lt;span class="c1"&gt;-- cycle guard&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hop&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;                            &lt;span class="c1"&gt;-- depth cap for safety&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hop&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;paths&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;node_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;target_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;hop&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Given &lt;code&gt;edges = {(1,2), (1,3), (2,4), (3,4), (4,5), (2,5)}&lt;/code&gt;, &lt;code&gt;:start_id = 1&lt;/code&gt;, &lt;code&gt;:target_id = 5&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Iteration&lt;/th&gt;
&lt;th&gt;Frontier&lt;/th&gt;
&lt;th&gt;Emitted rows&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0 (anchor)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(1, [1], 0)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{1}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;(2, [1,2], 1)&lt;/code&gt;, &lt;code&gt;(3, [1,3], 1)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{2, 3}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;(4, [1,2,4], 2)&lt;/code&gt;, &lt;code&gt;(5, [1,2,5], 2)&lt;/code&gt;, &lt;code&gt;(4, [1,3,4], 2)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{4, 4, 5}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;(5, [1,2,4,5], 3)&lt;/code&gt;, &lt;code&gt;(5, [1,3,4,5], 3)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{5, 5}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;none (5 has no outgoing)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;zero rows → terminate&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Outer query &lt;code&gt;WHERE node_id = 5 ORDER BY hop LIMIT 1&lt;/code&gt; returns &lt;code&gt;([1, 2, 5], 2)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;path&lt;/th&gt;
&lt;th&gt;hop&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;{1, 2, 5}&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Path array as memory&lt;/strong&gt;&lt;/strong&gt; — the recursive CTE has no other place to record "which nodes have I already visited on this branch." The path array is per-row, not per-CTE, so parallel branches keep independent memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cycle guard as pre-emit filter&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;WHERE NOT e.dst = ANY(p.path)&lt;/code&gt; runs &lt;em&gt;before&lt;/em&gt; the row is emitted. That way, we never insert a cycle-closing row, so the recursive term drains naturally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Depth cap as belt-and-braces&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;AND p.hop &amp;lt; 20&lt;/code&gt; is a safety net for graphs with unexpectedly long paths or subtle bugs in the cycle guard. Never rely on the dialect's max-recursion — engineer termination.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;&lt;code&gt;ORDER BY hop LIMIT 1&lt;/code&gt; for shortest&lt;/strong&gt;&lt;/strong&gt; — the recursive CTE enumerates every path; the outer aggregate picks the minimum. Correct but not efficient — for high-QPS shortest-path queries, use a graph DB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — for a graph with average branching factor &lt;code&gt;b&lt;/code&gt; and depth cap &lt;code&gt;d&lt;/code&gt;, worst-case is O(b^d). Path arrays and cycle guards keep it near O(edges) in practice, but skewed graphs can blow up. For millions of edges, move to a graph DB.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — graph&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Graph traversal problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/graph" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — recursion&lt;/span&gt;
&lt;strong&gt;Recursion practice problems&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/recursion" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  5. Bill-of-materials and dialect quirks
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;bill of materials sql&lt;/code&gt; rolls up child quantities into parent quantities, multiplying along every edge — and every dialect has its own max-recursion cap
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;a multi-level BOM is a DAG where each parent points to child components with a per-edge &lt;code&gt;quantity&lt;/code&gt; weight; the recursive CTE anchors on the top-level product, joins children in the recursive term, multiplies &lt;code&gt;accumulated_qty = parent_accumulated × edge_qty&lt;/code&gt;, and terminates when a component has no further sub-components; every dialect has its own default cap on recursion depth (Postgres effectively unbounded, MySQL 1000, SQL Server 100, Snowflake unbounded, BigQuery 500)&lt;/strong&gt;. Once you internalise "multiply along the edge, sum at the leaf," every &lt;code&gt;bill of materials sql&lt;/code&gt; interview surface collapses to a template.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe3jr6cfmlg037e17rxsg.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe3jr6cfmlg037e17rxsg.jpeg" alt="Visual diagram of a bill-of-materials rollup and dialect max-recursion matrix — left a 3-level BOM tree (bike → frame + wheels → aluminium + steel + rubber + spokes) with quantities multiplied along each edge; right a 5-dialect max-recursion matrix (Postgres, MySQL 8, SQL Server, Snowflake, BigQuery); on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The BOM data model.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;bom(parent_id, child_id, qty)&lt;/code&gt; — a "child_id appears &lt;code&gt;qty&lt;/code&gt; times inside parent_id."&lt;/li&gt;
&lt;li&gt;The graph is a DAG — a wheel appears in a bike and a scooter; there is no cycle by construction.&lt;/li&gt;
&lt;li&gt;Roll-up is the natural operation: "for a bike, how many spokes total?" — walk down, multiply along every edge.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The multiplication trick.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Anchor: &lt;code&gt;SELECT product_id, ARRAY[product_id] AS path, 1 AS accumulated_qty, 0 AS level FROM bom_top WHERE product_id = :root&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Recursive term: &lt;code&gt;SELECT b.child_id, cte.path || b.child_id, cte.accumulated_qty * b.qty, cte.level + 1 FROM bom b JOIN cte ON b.parent_id = cte.product_id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Termination: automatic — a leaf component has no &lt;code&gt;bom&lt;/code&gt; rows as parent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The rollup.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The CTE enumerates every path from root to leaf, carrying the accumulated quantity.&lt;/li&gt;
&lt;li&gt;To answer "how much of each component in total," sum the accumulated quantity per component:
&lt;code&gt;SELECT component_id, SUM(accumulated_qty) FROM cte GROUP BY component_id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;This handles the DAG shape: if a wheel appears in the bike and a wheel appears in the trailer (which is also inside the bike as a sub-assembly), summing at the outer query aggregates both occurrences.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Dialect max-recursion matrix.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dialect&lt;/th&gt;
&lt;th&gt;Default cap&lt;/th&gt;
&lt;th&gt;Override&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Postgres&lt;/td&gt;
&lt;td&gt;effectively unbounded (stack depth)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SET LOCAL max_stack_depth = '4MB'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL 8+&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cte_max_recursion_depth = 1000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SET cte_max_recursion_depth = 5000&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL Server&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MAXRECURSION 100&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;OPTION (MAXRECURSION 5000)&lt;/code&gt; per query&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snowflake&lt;/td&gt;
&lt;td&gt;effectively unbounded&lt;/td&gt;
&lt;td&gt;none needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BigQuery&lt;/td&gt;
&lt;td&gt;500 iterations&lt;/td&gt;
&lt;td&gt;none per-query; redesign&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DuckDB&lt;/td&gt;
&lt;td&gt;effectively unbounded&lt;/td&gt;
&lt;td&gt;none needed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;When the max recursion fires.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The engine raises a specific error: MySQL says "Recursive query aborted after N iterations", SQL Server says "Maximum recursion depth exceeded", BigQuery says "Recursive query iteration limit reached."&lt;/li&gt;
&lt;li&gt;The error is a hard stop — no partial results. Wrap your CTE in a &lt;code&gt;LIMIT&lt;/code&gt; on the outer query does &lt;em&gt;not&lt;/em&gt; prevent recursion from running to the cap.&lt;/li&gt;
&lt;li&gt;The right fix depends on the shape:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Legitimate deep recursion&lt;/strong&gt; — override the cap (SQL Server &lt;code&gt;OPTION (MAXRECURSION 5000)&lt;/code&gt;, MySQL &lt;code&gt;SET cte_max_recursion_depth = 5000&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infinite loop&lt;/strong&gt; — the recursive term never returns zero rows; find the bug (missing termination, missing cycle guard).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BigQuery hard 500 cap&lt;/strong&gt; — pre-flatten the closure with a materialised model.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When to switch to a graph database.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Depth &amp;gt; 100 with millions of nodes.&lt;/strong&gt; Recursive CTE cost grows with edges × depth. At depth &amp;gt; 100, a native graph engine's index-free adjacency wins by an order of magnitude.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High-QPS shortest-path queries.&lt;/strong&gt; Graph DBs (Neo4j, TigerGraph) are built for this; recursive CTEs enumerate all paths and pick min, which is O(paths) not O(edges).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frequent structural change plus deep queries.&lt;/strong&gt; Materialised closure tables become a maintenance burden; graph DBs handle live updates natively.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Path expressiveness matters.&lt;/strong&gt; Cypher's &lt;code&gt;MATCH (a)-[:KNOWS*1..3]-(b)&lt;/code&gt; is more readable than a recursive CTE for people who write graph queries daily.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on BOM.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"How do you sum the total quantity of each raw material for a bike?" — recursive CTE + &lt;code&gt;GROUP BY component_id, SUM(accumulated_qty)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;"What happens if a component appears in two sub-assemblies?" — the CTE emits two rows with the same &lt;code&gt;component_id&lt;/code&gt;; the outer &lt;code&gt;SUM&lt;/code&gt; adds them correctly.&lt;/li&gt;
&lt;li&gt;"What's the difference between &lt;code&gt;MAXRECURSION&lt;/code&gt; in SQL Server and &lt;code&gt;cte_max_recursion_depth&lt;/code&gt; in MySQL?" — same concept; different names. SQL Server is per-query hint; MySQL is a session variable.&lt;/li&gt;
&lt;li&gt;"When does a BOM query hit the recursion cap?" — very deep sub-assemblies (aerospace, complex electronics) or a buggy self-reference that never terminates.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — multi-level BOM rollup
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The setup.&lt;/strong&gt; A &lt;code&gt;bom(parent_id, child_id, qty)&lt;/code&gt; table. A bike has 1 frame and 2 wheels. A frame has 3 aluminium bars and 8 steel bolts. A wheel has 1 rubber tyre and 32 spokes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The multiplication.&lt;/strong&gt; Walking down, &lt;code&gt;accumulated_qty&lt;/code&gt; is multiplied by each edge's &lt;code&gt;qty&lt;/code&gt;. A spoke's accumulated qty is &lt;code&gt;1 (bike) × 2 (wheels per bike) × 32 (spokes per wheel) = 64&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The output.&lt;/strong&gt; For each component, the total quantity needed to build one bike.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given the BOM below, compute the total quantity of every raw material required to build 1 bike.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input — bom.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;parent_id&lt;/th&gt;
&lt;th&gt;child_id&lt;/th&gt;
&lt;th&gt;qty&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;bike&lt;/td&gt;
&lt;td&gt;frame&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bike&lt;/td&gt;
&lt;td&gt;wheel&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;frame&lt;/td&gt;
&lt;td&gt;aluminium_bar&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;frame&lt;/td&gt;
&lt;td&gt;steel_bolt&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;wheel&lt;/td&gt;
&lt;td&gt;rubber_tyre&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;wheel&lt;/td&gt;
&lt;td&gt;spoke&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;RECURSIVE&lt;/span&gt; &lt;span class="k"&gt;rollup&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="c1"&gt;-- anchor: the top-level product&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="s1"&gt;'bike'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;component_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;bigint&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;accumulated_qty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="mi"&gt;0&lt;/span&gt;            &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;level&lt;/span&gt;
    &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;

    &lt;span class="c1"&gt;-- recursive: children of the frontier, quantity multiplied along the edge&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;child_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;accumulated_qty&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;level&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;rollup&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
    &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;bom&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;component_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent_id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;component_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accumulated_qty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_qty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;level&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;           &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;max_depth&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;rollup&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;component_id&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s1"&gt;'bike'&lt;/span&gt;      &lt;span class="c1"&gt;-- exclude the top-level product from the tally&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;component_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;total_qty&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Anchor.&lt;/strong&gt; Emits &lt;code&gt;('bike', 1, 0)&lt;/code&gt;. Frontier &lt;code&gt;{bike}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recursive step 1.&lt;/strong&gt; From &lt;code&gt;bike&lt;/code&gt;: children &lt;code&gt;frame&lt;/code&gt; (qty 1) and &lt;code&gt;wheel&lt;/code&gt; (qty 2). Emits &lt;code&gt;('frame', 1*1 = 1, 1)&lt;/code&gt; and &lt;code&gt;('wheel', 1*2 = 2, 1)&lt;/code&gt;. Frontier &lt;code&gt;{frame, wheel}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recursive step 2.&lt;/strong&gt; From &lt;code&gt;frame&lt;/code&gt;: children &lt;code&gt;aluminium_bar&lt;/code&gt; (qty 3) and &lt;code&gt;steel_bolt&lt;/code&gt; (qty 8). Emits &lt;code&gt;('aluminium_bar', 1*3 = 3, 2)&lt;/code&gt; and &lt;code&gt;('steel_bolt', 1*8 = 8, 2)&lt;/code&gt;. From &lt;code&gt;wheel&lt;/code&gt;: children &lt;code&gt;rubber_tyre&lt;/code&gt; (qty 1) and &lt;code&gt;spoke&lt;/code&gt; (qty 32). Emits &lt;code&gt;('rubber_tyre', 2*1 = 2, 2)&lt;/code&gt; and &lt;code&gt;('spoke', 2*32 = 64, 2)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recursive step 3.&lt;/strong&gt; No leaf component has a &lt;code&gt;bom&lt;/code&gt; row as parent → recursive term emits zero rows → CTE terminates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outer query.&lt;/strong&gt; Groups by &lt;code&gt;component_id&lt;/code&gt;, sums &lt;code&gt;accumulated_qty&lt;/code&gt;. Every leaf has exactly one row here (DAG has no shared component), so the sum equals the accumulated. Filters out &lt;code&gt;bike&lt;/code&gt; (the top-level product itself).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;component_id&lt;/th&gt;
&lt;th&gt;total_qty&lt;/th&gt;
&lt;th&gt;max_depth&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;spoke&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;steel_bolt&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;aluminium_bar&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;wheel&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rubber_tyre&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;frame&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; BOM rollup multiplies along every edge and sums at the outer query. The multiplication happens &lt;em&gt;inside&lt;/em&gt; the recursive term (never after); the summing happens &lt;em&gt;outside&lt;/em&gt; (never inside). Get this split right and every BOM question follows.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — dialect max-recursion overrides
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Why the caps exist.&lt;/strong&gt; Every dialect has an implicit safety cap to prevent runaway recursion from burning CPU forever. The cap is intentionally low so that a buggy CTE fails fast.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When to override.&lt;/strong&gt; Deep BOMs (aerospace: 30+ levels), deep organisational structures (multinationals: 20+), deep dataset lineage graphs. The default cap is not enough for these workloads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How to override per dialect.&lt;/strong&gt; MySQL uses a session variable. SQL Server uses a per-query hint. Postgres has no explicit cap — you tune &lt;code&gt;max_stack_depth&lt;/code&gt; at the server level. BigQuery has no per-query override — you must pre-flatten.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Show the max-recursion override syntax in MySQL 8, SQL Server, and Postgres. What happens if the cap fires in each dialect?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(none — configuration syntax varies)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- MySQL 8+ — session variable, applies to subsequent CTEs in this session&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;SESSION&lt;/span&gt; &lt;span class="n"&gt;cte_max_recursion_depth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;RECURSIVE&lt;/span&gt; &lt;span class="n"&gt;deep&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
    &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;deep&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;deep&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- SQL Server — per-query hint, no session change needed&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;deep&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
    &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;deep&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;deep&lt;/span&gt;
&lt;span class="k"&gt;OPTION&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MAXRECURSION&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Postgres — no explicit cap; tune server-level max_stack_depth if needed&lt;/span&gt;
&lt;span class="c1"&gt;-- (typically only needed for depth &amp;gt; 10K)&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;LOCAL&lt;/span&gt; &lt;span class="n"&gt;max_stack_depth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'4MB'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;-- session-scoped&lt;/span&gt;

&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;RECURSIVE&lt;/span&gt; &lt;span class="n"&gt;deep&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
    &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;deep&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;deep&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- BigQuery — no per-query override; 500-iteration cap is hard&lt;/span&gt;
&lt;span class="c1"&gt;-- If depth &amp;gt; 500, pre-flatten with a materialised model&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;MySQL.&lt;/strong&gt; &lt;code&gt;cte_max_recursion_depth&lt;/code&gt; is a session variable, default 1000. Setting it to 5000 lets deeper CTEs run. When the cap fires, MySQL errors with "Recursive query aborted after N iterations".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL Server.&lt;/strong&gt; &lt;code&gt;OPTION (MAXRECURSION N)&lt;/code&gt; is a per-query hint appended to the outer &lt;code&gt;SELECT&lt;/code&gt;. Default 100. &lt;code&gt;MAXRECURSION 0&lt;/code&gt; means unbounded (dangerous — use only when you have engineered termination in the recursive term). When the cap fires, SQL Server errors with "Maximum recursion depth of N has been exhausted before statement completion".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres.&lt;/strong&gt; No dedicated cap. Recursion is bounded by &lt;code&gt;max_stack_depth&lt;/code&gt; (server-level, typically 2 MB) — you rarely need to touch this for reasonable CTEs. When stack overflow occurs, Postgres crashes the query (not the server) with "stack depth limit exceeded". In practice, Postgres CTEs recurse into the tens of thousands without issue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BigQuery.&lt;/strong&gt; 500 iterations, hard-coded. No override. If your workload needs deeper recursion, materialise a closure table incrementally via scheduled query or dbt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;General pattern.&lt;/strong&gt; Always specify termination in the recursive term (&lt;code&gt;WHERE n &amp;lt; bound&lt;/code&gt;, cycle guard). Rely on the dialect cap as a safety net, never as an intended stopping mechanism.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output — configuration matrix.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dialect&lt;/th&gt;
&lt;th&gt;Default cap&lt;/th&gt;
&lt;th&gt;Override syntax&lt;/th&gt;
&lt;th&gt;Error message when fired&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Postgres&lt;/td&gt;
&lt;td&gt;stack (~10-100K)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SET max_stack_depth&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;"stack depth limit exceeded"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL 8+&lt;/td&gt;
&lt;td&gt;1 000&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SET cte_max_recursion_depth = N&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;"Recursive query aborted after N iterations"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL Server&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;&lt;code&gt;OPTION (MAXRECURSION N)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;"Maximum recursion depth of N has been exhausted"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snowflake&lt;/td&gt;
&lt;td&gt;unbounded&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;(watch credit cost)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BigQuery&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;none per-query&lt;/td&gt;
&lt;td&gt;"Recursive query iteration limit reached"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DuckDB&lt;/td&gt;
&lt;td&gt;unbounded&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;(watch memory)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For deep BOMs or lineage graphs, override the cap and engineer termination in the recursive term. Never bump the cap to hide an infinite loop — fix the loop instead.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — when to switch to a graph database
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The signal to switch.&lt;/strong&gt; Deep + wide + high-QPS + frequent updates → the four ingredients that make recursive CTEs stop being the right answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Depth alone is not enough.&lt;/strong&gt; A 30-level BOM queried once a day runs fine on a recursive CTE. Add 500 QPS and the CPU becomes the bottleneck; add continuous updates and the closure-table approach becomes lossy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where graph DBs win.&lt;/strong&gt; Index-free adjacency (traversing an edge is O(1) instead of a B-tree lookup), path expressions (&lt;code&gt;(a)-[:KNOWS*1..3]-(b)&lt;/code&gt; in Cypher), and native shortest-path algorithms.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; For a 50M-node dependency graph with continuous updates and 200 shortest-path queries per second, would you use recursive CTE, closure table, or graph DB? Justify.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Constraint&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Nodes&lt;/td&gt;
&lt;td&gt;50 M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Edges&lt;/td&gt;
&lt;td&gt;250 M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Update rate&lt;/td&gt;
&lt;td&gt;1000 edge changes / sec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Query rate&lt;/td&gt;
&lt;td&gt;200 shortest-path / sec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Depth range&lt;/td&gt;
&lt;td&gt;1-15&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Decision:
  - Recursive CTE at query time?
    ✗ 200 QPS × O(edges × depth) = CPU-bound; every query is 100-500 ms.

  - Closure table + nightly rebuild?
    ✗ 1000 updates / sec invalidates the closure faster than we can rebuild.
      Even an incremental maintainer would lag behind.

  - Graph DB (Neo4j / TigerGraph / Amazon Neptune)?
    ✓ Index-free adjacency: each hop is O(1).
    ✓ Native shortest-path algos (Dijkstra, A*) run in ms.
    ✓ Live updates handled natively; no closure-table catchup lag.
    ✓ Path expression is 1 line of Cypher, not 30 lines of SQL.

Verdict: graph DB. Migrate the graph out of the RDBMS; keep the OLTP tables that generate edges in Postgres, ETL edges into Neo4j via a Kafka topic.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Recursive CTE at query time.&lt;/strong&gt; Each query walks up to depth 15 with a fanout that could be hundreds of neighbours per hop. At 200 QPS, we need to serve 200 × O(edges × depth) queries per second. Even at 100 ms per query, that's 20 CPU-seconds per wallclock second — several dedicated CPUs, per node in the cluster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Closure table + nightly rebuild.&lt;/strong&gt; A closure table for 50M nodes with depth 15 could be tens of billions of rows. Rebuilding it once a day is expensive. And with 1000 updates/sec, the closure is stale seconds after the rebuild.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Closure table + incremental maintenance.&lt;/strong&gt; An incremental closure maintainer runs on every edge change: for each new edge &lt;code&gt;(u, v)&lt;/code&gt;, find all rows &lt;code&gt;(x, u)&lt;/code&gt; in the closure and add &lt;code&gt;(x, v)&lt;/code&gt;. Feasible for lower update rates; at 1000/sec, the maintainer becomes the bottleneck.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Graph DB.&lt;/strong&gt; Native graph engines (Neo4j, TigerGraph, Amazon Neptune) store edges as pointers between node records — traversing an edge is one memory dereference, not a B-tree lookup. 200 QPS with depth-15 shortest-path is a normal load.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migration path.&lt;/strong&gt; Keep OLTP tables in Postgres. Emit every edge change to a Kafka topic. Consume in the graph DB. Serve reads from the graph DB. This is the standard pattern; it avoids rewriting the OLTP layer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Feasibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Recursive CTE at 200 QPS on 50M-node graph&lt;/td&gt;
&lt;td&gt;✗ CPU-bound&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Closure table + nightly rebuild&lt;/td&gt;
&lt;td&gt;✗ stale within seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Closure table + incremental maintainer&lt;/td&gt;
&lt;td&gt;✗ maintainer is the bottleneck&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Graph DB with edge stream from Postgres&lt;/td&gt;
&lt;td&gt;✓ scales; ms-latency reads&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; When any two of (depth &amp;gt; 20, nodes &amp;gt; 10M, QPS &amp;gt; 100, updates &amp;gt; 100/sec) hold at once, plan a graph DB migration. Recursive CTEs are the right hammer for shallow and low-QPS; deep and hot workloads need a different toolbox.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on end-to-end BOM roll-up
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might frame this as: "Design a BOM roll-up that supports multi-level assemblies, tolerates a 30-level max depth on a MySQL 8 instance, and reports total quantity plus maximum depth per raw component. Show the SQL, the dialect config, and the failure modes."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a recursive CTE with dialect override and quantity multiplication
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- MySQL 8+ — session config for deep BOMs&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;SESSION&lt;/span&gt; &lt;span class="n"&gt;cte_max_recursion_depth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;RECURSIVE&lt;/span&gt; &lt;span class="k"&gt;rollup&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;root_id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;component_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="mi"&gt;1&lt;/span&gt;        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;accumulated_qty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="mi"&gt;0&lt;/span&gt;        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;level&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(:&lt;/span&gt;&lt;span class="n"&gt;root_id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;CHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;
    &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;child_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;accumulated_qty&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;level&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;CONCAT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;child_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;rollup&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
    &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;bom&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;component_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent_id&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;level&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;                                        &lt;span class="c1"&gt;-- engineered safety cap&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;FIND_IN_SET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;child_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;','&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;   &lt;span class="c1"&gt;-- guard against accidental cycles&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;component_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accumulated_qty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_qty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;level&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;           &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;max_depth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;             &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;occurrences&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;rollup&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;component_id&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;root_id&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;component_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;total_qty&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Given a 3-level BOM (&lt;code&gt;bike → frame → aluminium/steel; bike → wheel → rubber/spoke&lt;/code&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Iteration&lt;/th&gt;
&lt;th&gt;Frontier&lt;/th&gt;
&lt;th&gt;Emitted rows&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0 (anchor)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(bike, 1, 0, 'bike')&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{bike}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;(frame, 1, 1, 'bike&amp;gt;frame')&lt;/code&gt;, &lt;code&gt;(wheel, 2, 1, 'bike&amp;gt;wheel')&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{frame, wheel}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;(aluminium, 3, 2, ...)&lt;/code&gt;, &lt;code&gt;(steel, 8, 2, ...)&lt;/code&gt;, &lt;code&gt;(rubber, 2, 2, ...)&lt;/code&gt;, &lt;code&gt;(spoke, 64, 2, ...)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;leaves&lt;/td&gt;
&lt;td&gt;zero rows → terminate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Outer query aggregates: &lt;code&gt;spoke → 64&lt;/code&gt;, &lt;code&gt;steel → 8&lt;/code&gt;, &lt;code&gt;aluminium → 3&lt;/code&gt;, &lt;code&gt;wheel → 2&lt;/code&gt;, &lt;code&gt;rubber → 2&lt;/code&gt;, &lt;code&gt;frame → 1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;FIND_IN_SET&lt;/code&gt; guard uses the string path (MySQL) to detect accidental cycles; the &lt;code&gt;level &amp;lt; 50&lt;/code&gt; cap is a belt-and-braces safety net well under the session's &lt;code&gt;cte_max_recursion_depth&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;component_id&lt;/th&gt;
&lt;th&gt;total_qty&lt;/th&gt;
&lt;th&gt;max_depth&lt;/th&gt;
&lt;th&gt;occurrences&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;spoke&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;steel_bolt&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;aluminium_bar&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;wheel&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rubber_tyre&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;frame&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Quantity multiplied inside the recursive term&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;accumulated_qty * b.qty&lt;/code&gt; runs on every recursive step. This is the entire point of BOM rollup; skipping it gives you a component list without amounts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Session config for MySQL cap&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;SET SESSION cte_max_recursion_depth = 100&lt;/code&gt; bumps the MySQL default from 1000 down (or up) to a value that matches the workload. Sensible caps prevent runaway CTEs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Path column as cycle guard&lt;/strong&gt;&lt;/strong&gt; — even for BOMs (which are DAGs by design), the &lt;code&gt;FIND_IN_SET&lt;/code&gt; guard catches accidental data-entry cycles that would otherwise cook the query. Cheap insurance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Engineered level cap&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;WHERE r.level &amp;lt; 50&lt;/code&gt; gives you a &lt;em&gt;predictable&lt;/em&gt; max depth that is independent of the dialect's implicit cap. If the workload's real depth is 30, the level cap is a safety net; if it fires unexpectedly, you know something is wrong before the dialect's error message surfaces.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — O(edges) per traversal since each edge is walked once. For a 100K-part BOM at depth 30, that's &amp;lt; 100ms on modern Postgres/MySQL/SQL Server. For BigQuery, the 500-iteration cap prevents very deep BOMs; use a materialised closure table instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — CTE · hard&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Hard recursive CTE problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/cte/difficulty/hard" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;


&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — SQL&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL interview problem library&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;





&lt;h2&gt;
  
  
  Cheat sheet — recursive CTE recipes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Minimum viable recursive CTE.&lt;/strong&gt; &lt;code&gt;WITH RECURSIVE cte AS (anchor SELECT UNION ALL recursive SELECT joining cte) SELECT ... FROM cte;&lt;/code&gt;. Two SELECTs, one &lt;code&gt;UNION ALL&lt;/code&gt;, one termination condition. That is the whole shape.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Running-numbers series generator.&lt;/strong&gt; &lt;code&gt;WITH RECURSIVE nums AS (SELECT 1 AS n UNION ALL SELECT n + 1 FROM nums WHERE n &amp;lt; :N) SELECT n FROM nums;&lt;/code&gt;. The "hello world" — sanity-check dialect support and syntax in 4 lines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ancestors of a row.&lt;/strong&gt; Anchor &lt;code&gt;WHERE id = :leaf&lt;/code&gt;; recursive term &lt;code&gt;JOIN employees e ON cte.manager_id = e.id&lt;/code&gt;. Walks up until the root's &lt;code&gt;manager_id IS NULL&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All descendants of a row.&lt;/strong&gt; Anchor &lt;code&gt;WHERE id = :root&lt;/code&gt;; recursive term &lt;code&gt;JOIN employees e ON e.manager_id = cte.id&lt;/code&gt;. Add &lt;code&gt;level + 1&lt;/code&gt; and &lt;code&gt;path || e.id&lt;/code&gt; for depth and ancestry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BFS from a start node.&lt;/strong&gt; Anchor emits &lt;code&gt;(start, 0, [start])&lt;/code&gt;. Recursive term joins edges to the frontier, filters &lt;code&gt;NOT other = ANY(path)&lt;/code&gt;, appends to path, increments hop. &lt;code&gt;ORDER BY hop&lt;/code&gt; in the outer query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cycle detection idiom.&lt;/strong&gt; Carry a path array; filter &lt;code&gt;WHERE NOT dst = ANY(path)&lt;/code&gt; on the recursive term. Explicit cycle reporting: emit &lt;code&gt;is_cycle := dst = ANY(path)&lt;/code&gt; and filter in the outer query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-level BOM rollup with quantity multiplication.&lt;/strong&gt; Recursive term multiplies &lt;code&gt;parent.accumulated_qty * edge.qty&lt;/code&gt;. Outer query groups by component and sums accumulated quantities.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres &lt;code&gt;SEARCH&lt;/code&gt; clause.&lt;/strong&gt; &lt;code&gt;SEARCH DEPTH FIRST BY col SET seq&lt;/code&gt; or &lt;code&gt;SEARCH BREADTH FIRST BY col SET seq&lt;/code&gt;. Adds a &lt;code&gt;seq&lt;/code&gt; column that sorts DFS/BFS without hand-coding a path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres &lt;code&gt;CYCLE&lt;/code&gt; clause.&lt;/strong&gt; &lt;code&gt;CYCLE col SET is_cycle USING path&lt;/code&gt;. Auto-detects cycles, stops expanding cycle-flagged rows, exposes &lt;code&gt;is_cycle&lt;/code&gt; and &lt;code&gt;path&lt;/code&gt; columns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MySQL max-recursion override.&lt;/strong&gt; &lt;code&gt;SET SESSION cte_max_recursion_depth = 5000;&lt;/code&gt; before your CTE. Default is 1000. Session-scoped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL Server max-recursion override.&lt;/strong&gt; &lt;code&gt;OPTION (MAXRECURSION 5000)&lt;/code&gt; appended to the outer &lt;code&gt;SELECT&lt;/code&gt;. Default is 100. Per-query. &lt;code&gt;MAXRECURSION 0&lt;/code&gt; is unbounded — dangerous.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BigQuery hard cap.&lt;/strong&gt; 500 iterations. No per-query override. Deep workloads must pre-flatten via materialised closure model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;postgres recursive cte&lt;/code&gt; deep tuning.&lt;/strong&gt; Postgres has no per-query cap; the practical limit is &lt;code&gt;max_stack_depth&lt;/code&gt;. &lt;code&gt;SET LOCAL max_stack_depth = '4MB'&lt;/code&gt; handles depths into the tens of thousands.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When NOT to use a recursive CTE.&lt;/strong&gt; Depth &amp;gt; 100 with high QPS → closure table or graph DB. Cyclic graph with shortest-path queries at scale → graph DB. Simple 2-level lookup → plain join, no recursion needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Closure-table pattern.&lt;/strong&gt; Rebuild once daily from a recursive CTE; store &lt;code&gt;(ancestor_id, descendant_id, depth)&lt;/code&gt; with a composite PK. Reads become a single-index lookup.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is a SQL recursive CTE?
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;sql recursive cte&lt;/code&gt; is a Common Table Expression that references itself in its own definition, enabling iterative traversal of hierarchies, trees, graphs, and multi-level structures without application-side loops. The shape is fixed: &lt;code&gt;WITH RECURSIVE cte AS (anchor SELECT UNION ALL recursive SELECT joining cte) SELECT ... FROM cte&lt;/code&gt;. The anchor runs once and seeds the CTE with base rows; the recursive term joins the CTE to a base table and runs repeatedly until it emits zero rows. Every dialect except SQL Server (which infers recursion) requires the &lt;code&gt;RECURSIVE&lt;/code&gt; keyword. Common uses: &lt;code&gt;sql hierarchy query&lt;/code&gt; for org charts, &lt;code&gt;graph traversal sql&lt;/code&gt; for friendship networks, &lt;code&gt;bill of materials sql&lt;/code&gt; for multi-level assemblies, running-number series for date-range generation, and materialised closure-table rebuilds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does MySQL support recursive CTEs?
&lt;/h3&gt;

&lt;p&gt;Yes — MySQL added first-class recursive CTE support in MySQL 8.0 (2018). The syntax is &lt;code&gt;WITH RECURSIVE cte AS (anchor UNION ALL recursive_term) SELECT ...&lt;/code&gt;, identical to Postgres. Two MySQL-specific things to know: the default max recursion is &lt;code&gt;cte_max_recursion_depth = 1000&lt;/code&gt; (bump it with &lt;code&gt;SET SESSION cte_max_recursion_depth = 5000&lt;/code&gt; if your BOM or lineage graph exceeds 1000 levels), and MySQL does not have first-class &lt;code&gt;SEARCH&lt;/code&gt; or &lt;code&gt;CYCLE&lt;/code&gt; clauses like Postgres — you roll your own path array and &lt;code&gt;FIND_IN_SET&lt;/code&gt;-style cycle guard using string paths (or JSON arrays on 8.0.17+). Older MySQL 5.7 and earlier do not support recursive CTEs — you either upgrade or fall back to application-side loops.&lt;/p&gt;

&lt;h3&gt;
  
  
  When should I use &lt;code&gt;UNION ALL&lt;/code&gt; vs &lt;code&gt;UNION&lt;/code&gt; in a recursive CTE?
&lt;/h3&gt;

&lt;p&gt;Almost always &lt;code&gt;UNION ALL&lt;/code&gt;. It preserves every row emitted by the recursive term, which is what you want. &lt;code&gt;UNION&lt;/code&gt; (implicit &lt;code&gt;SELECT DISTINCT&lt;/code&gt;) silently dedupes across generations, adding sort/hash overhead and hiding intent. The one place &lt;code&gt;UNION&lt;/code&gt; sometimes "helps" is a cyclic graph where deduping accidentally terminates the recursion — but that is a happy accident, not an engineered termination. The correct fix for a cyclic graph is a cycle guard (path array + &lt;code&gt;NOT node = ANY(path)&lt;/code&gt;), not &lt;code&gt;UNION&lt;/code&gt;. Default to &lt;code&gt;UNION ALL&lt;/code&gt;; reach for &lt;code&gt;UNION&lt;/code&gt; only after measuring a specific dedup win and articulating exactly which duplicates you are dropping.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I detect cycles in a recursive CTE?
&lt;/h3&gt;

&lt;p&gt;Accumulate a &lt;code&gt;path&lt;/code&gt; array on every recursive step, then filter &lt;code&gt;WHERE NOT next_node = ANY(path)&lt;/code&gt; before emitting. On Postgres, use array operators natively (&lt;code&gt;ARRAY[node_id]&lt;/code&gt;, &lt;code&gt;path || node_id&lt;/code&gt;, &lt;code&gt;= ANY&lt;/code&gt;). On MySQL, use &lt;code&gt;CONCAT&lt;/code&gt; for a string path and &lt;code&gt;FIND_IN_SET&lt;/code&gt; for the guard. On SQL Server, use &lt;code&gt;CONCAT&lt;/code&gt; and a &lt;code&gt;LIKE&lt;/code&gt; or a JSON array. Postgres 14+ ships a declarative &lt;code&gt;CYCLE col SET is_cycle USING path&lt;/code&gt; clause that does all the bookkeeping for you and stops the recursion the moment a cycle is detected. Always add a cycle guard for anything with multi-parent or bidirectional edges, even if today's data is cycle-free — tomorrow's bug will insert one.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I compute the depth of every node in an org chart?
&lt;/h3&gt;

&lt;p&gt;Anchor the recursive CTE on the root (&lt;code&gt;WHERE manager_id IS NULL&lt;/code&gt;) with &lt;code&gt;level = 0&lt;/code&gt;. In the recursive term, join &lt;code&gt;employees e ON e.manager_id = cte.id&lt;/code&gt; and select &lt;code&gt;cte.level + 1&lt;/code&gt; as the new &lt;code&gt;level&lt;/code&gt;. Termination is automatic — leaves have no children, so the frontier drains. To get depth-first traversal order, carry a &lt;code&gt;path&lt;/code&gt; array (&lt;code&gt;cte.path || e.id&lt;/code&gt;) and &lt;code&gt;ORDER BY path&lt;/code&gt; in the outer query. Depth-first is what you want when rendering a tree in the UI; breadth-first (&lt;code&gt;ORDER BY level, name&lt;/code&gt;) is what you want when reporting "all employees at level N." This is the canonical &lt;code&gt;sql hierarchy query&lt;/code&gt; and it works identically across Postgres, MySQL 8+, SQL Server (drop the &lt;code&gt;RECURSIVE&lt;/code&gt; keyword), Snowflake, BigQuery, and DuckDB.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between a recursive CTE and a graph database query?
&lt;/h3&gt;

&lt;p&gt;A recursive CTE runs inside your OLTP database against a normal table with a self-referential FK — no new system to operate, no ETL to maintain, and depth-≤-10 workloads are typically sub-second. A graph database (Neo4j, TigerGraph, Amazon Neptune) stores edges as native pointers between node records — traversing an edge is O(1) memory dereference rather than a B-tree lookup, path expressions like Cypher's &lt;code&gt;(a)-[:KNOWS*1..3]-(b)&lt;/code&gt; are one line instead of thirty, and native shortest-path algorithms (Dijkstra, A*) run in milliseconds even on multi-million-node graphs. Reach for a recursive CTE for shallow depth (≤ 10) and moderate QPS (≤ 100), reach for a graph DB when two or more of "deep + wide + high-QPS + high-update-rate" hold at once. The &lt;code&gt;postgres recursive cte&lt;/code&gt; covers 80% of production hierarchy and BOM workloads; the remaining 20% is where graph DBs earn their operational cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practice on PipeCode
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Drill the &lt;a href="https://pipecode.ai/explore/practice/topic/cte" rel="noopener noreferrer"&gt;CTE practice library →&lt;/a&gt; for the recursive-CTE shape, anchor + recursive-term probes, and dialect-specific overrides.&lt;/li&gt;
&lt;li&gt;Rehearse on &lt;a href="https://pipecode.ai/explore/practice/topic/cte/difficulty/hard" rel="noopener noreferrer"&gt;hard-difficulty CTE problems →&lt;/a&gt; when the interviewer wants deep BOM rollups and multi-level ancestry queries.&lt;/li&gt;
&lt;li&gt;Sharpen &lt;a href="https://pipecode.ai/explore/practice/topic/tree" rel="noopener noreferrer"&gt;tree traversal problems →&lt;/a&gt; for the org-chart / category-tree / filesystem family.&lt;/li&gt;
&lt;li&gt;Stack the &lt;a href="https://pipecode.ai/explore/practice/topic/graph" rel="noopener noreferrer"&gt;graph problems library →&lt;/a&gt; for BFS, cycle detection, and shortest-path variants.&lt;/li&gt;
&lt;li&gt;Layer the &lt;a href="https://pipecode.ai/explore/practice/topic/recursion" rel="noopener noreferrer"&gt;recursion library →&lt;/a&gt; for anchor + recursive-step reasoning that shows up outside CTEs too.&lt;/li&gt;
&lt;li&gt;For general SQL sharpening, work through the &lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;SQL problem library →&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Complement the CTE grind with the &lt;a href="https://pipecode.ai/explore/practice/topic/ctes" rel="noopener noreferrer"&gt;CTEs topic collection →&lt;/a&gt; for shorter warm-up drills.&lt;/li&gt;
&lt;li&gt;For the broader SQL interview surface, take the &lt;a href="https://pipecode.ai/explore/courses/sql-for-data-engineering-interviews-from-zero-to-faang" rel="noopener noreferrer"&gt;SQL for Data Engineering course →&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/" rel="noopener noreferrer"&gt;Pipecode.ai&lt;/a&gt; is Leetcode for Data Engineering — every recursive CTE recipe above ships with hands-on practice rooms where you write the anchor + recursive term, wire the cycle guard, and roll up a multi-level BOM against real graded inputs. PipeCode pairs every reading with 450+ DE-focused problems and a real-time scoring engine, so you never have to wonder whether your `sql recursive cte` answer holds up under a senior interviewer's depth probes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/cte" rel="noopener noreferrer"&gt;Practice recursive CTEs now →&lt;/a&gt;&lt;br&gt;
&lt;a href="https://pipecode.ai/explore/practice/topic/graph" rel="noopener noreferrer"&gt;Graph traversal drills →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>sql</category>
      <category>interview</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>LLM Evaluation Pipelines: Golden Sets, Cosine Similarity, LLM-as-Judge for Data Teams</title>
      <dc:creator>Gowtham Potureddi</dc:creator>
      <pubDate>Fri, 10 Jul 2026 16:37:24 +0000</pubDate>
      <link>https://dev.to/gowthampotureddi/llm-evaluation-pipelines-golden-sets-cosine-similarity-llm-as-judge-for-data-teams-5fhm</link>
      <guid>https://dev.to/gowthampotureddi/llm-evaluation-pipelines-golden-sets-cosine-similarity-llm-as-judge-for-data-teams-5fhm</guid>
      <description>&lt;p&gt;&lt;strong&gt;&lt;code&gt;llm evaluation&lt;/code&gt;&lt;/strong&gt; is the discipline that separates a demoable prototype from an LLM feature you can put in front of paying customers — and, in 2026, it is squarely a data-engineering problem, not a research one. Every product team ships at least one language-model feature, every prompt change silently reshapes the answer distribution, and every retrieval index update touches the same measured quality surface. Without a versioned &lt;code&gt;golden set&lt;/code&gt;, a numeric metric, and a &lt;code&gt;llm eval pipeline&lt;/code&gt; that runs on every pull request, "did we make it worse?" becomes a Slack argument instead of a graph. The data-engineering signal an interviewer looks for is exactly the one that keeps a product shippable: rigorous inputs, deterministic scoring, cost-aware judge design, and CI hooks that block a regression before it ever reaches production.&lt;/p&gt;

&lt;p&gt;This guide is the senior-DE walkthrough you wished existed the first time an interviewer asked "walk me through how you'd design a &lt;code&gt;llm as judge&lt;/code&gt; scorer without letting the judge outvote your labels" or "when do you trust &lt;code&gt;cosine similarity&lt;/code&gt; and when do you escalate to a rubric-scoring judge?" or "how do you keep a &lt;code&gt;rag evaluation&lt;/code&gt; harness green through a text-embedding upgrade?" It walks through why LLM evaluation is a data-eng problem (golden-set discipline, drift monitoring, metric hygiene), how to curate and version a golden set the way you version code, the mechanics of cosine similarity scoring with &lt;code&gt;text-embedding-3&lt;/code&gt; and threshold calibration, the LLM-as-judge rubric with structured JSON output and cost control, and the CI + production monitoring loop that catches regressions before they ship. Each section pairs a teaching block with a Solution-Tail interview answer — code, a step-by-step trace, an output table, then a concept-by-concept breakdown of why it works. &lt;code&gt;ragas&lt;/code&gt;, &lt;code&gt;deepeval&lt;/code&gt;, and &lt;code&gt;promptfoo&lt;/code&gt; show up where they earn their place; the rest is the plumbing you'll build yourself.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0qzgavu1j9iysgj7z35g.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0qzgavu1j9iysgj7z35g.jpeg" alt="PipeCode blog header for LLM evaluation — bold white headline 'LLM Evaluation Pipelines' over a hero composition of a golden-set scroll on the left, a cosine gauge in the middle, and a judge glyph on the right, on a dark gradient." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you want &lt;strong&gt;hands-on reps&lt;/strong&gt; immediately after reading, drill the &lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;ETL practice library →&lt;/a&gt;, rehearse on the &lt;a href="https://pipecode.ai/explore/practice/topic/data-analysis" rel="noopener noreferrer"&gt;data-analysis practice library →&lt;/a&gt;, and sharpen the tuning axis with the &lt;a href="https://pipecode.ai/explore/practice/topic/optimization" rel="noopener noreferrer"&gt;optimization practice library →&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;On this page&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why LLM evaluation is a data-eng problem&lt;/li&gt;
&lt;li&gt;Golden sets — curation and versioning&lt;/li&gt;
&lt;li&gt;Cosine similarity + embedding-based scoring&lt;/li&gt;
&lt;li&gt;LLM-as-judge — GPT-4-class rubric scorer&lt;/li&gt;
&lt;li&gt;CI integration + production monitoring&lt;/li&gt;
&lt;li&gt;Cheat sheet — LLM eval recipes&lt;/li&gt;
&lt;li&gt;Frequently asked questions&lt;/li&gt;
&lt;li&gt;Practice on PipeCode&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1. Why LLM evaluation is a data-eng problem
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Evaluation, not modeling, is what keeps LLM features shippable — and it is pipelines, not papers
&lt;/h3&gt;

&lt;p&gt;The one-sentence invariant: &lt;strong&gt;an LLM feature without a versioned golden set and a nightly &lt;code&gt;llm eval pipeline&lt;/code&gt; is a research prototype pretending to be a product, and every prompt change is a silent regression risk until the evaluation harness proves otherwise&lt;/strong&gt;. In 2026, most teams do not train models — they compose a base model with a prompt, a retrieval index, and a set of tools. The engineering surface that decides whether the product stays good is not "which model did you fine-tune," it is "which inputs did you preserve, which metric did you compute, and did the CI harness block yesterday's bad PR." That is a data-engineering problem end to end: curated inputs, deterministic scoring, versioned artefacts, and observable outputs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four "must-answer" axes interviewers actually probe.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Golden set.&lt;/strong&gt; A curated, versioned set of &lt;code&gt;(input, expected_output, metadata)&lt;/code&gt; tuples that the harness scores against on every change. The senior signal is &lt;em&gt;how&lt;/em&gt; you curate it: SME-labelled, class-balanced, git-hashed, growing every week. "We use the same 20 examples we tested with in the demo" is a fail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metric.&lt;/strong&gt; Which numeric score are you computing per row and how do you aggregate it? Exact match is too strict; free-form BLEU is meaningless; cosine similarity on embeddings is a strong baseline; &lt;code&gt;llm as judge&lt;/code&gt; rubric scores are the modern default. The senior signal is knowing &lt;em&gt;which metric for which task&lt;/em&gt; and how they fail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM-as-judge.&lt;/strong&gt; A GPT-4-class model scoring the prediction against the expected answer using a rubric, returning a structured score plus a rationale. Cheap enough to run per PR, expensive enough that you cache and sample intelligently. The senior signal is understanding judge bias, prompt sensitivity, and how you'd guard against them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI integration.&lt;/strong&gt; The harness runs on every pull request, blocks the merge if the aggregate score drops by more than a configured threshold (typically 5 percent), and posts the diff into the PR comment. In production, you sample live traffic through the same harness and alert on drift. The senior signal is treating quality as a build gate, not a lagging indicator.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why this is a data-engineering problem, not a modeling one.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The scarce resource is data, not compute.&lt;/strong&gt; The bottleneck to shipping a better LLM feature is &lt;em&gt;not&lt;/em&gt; a bigger model — it is a bigger, cleaner, better-labelled golden set plus a metric you trust. Both are data problems.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The pipeline shape is DAG-native.&lt;/strong&gt; Load golden set → run predictions → embed → compare to expected → score → aggregate → alert. That is a DAG. It runs on the same Airflow / Dagster / Prefect scheduler you already have. The primitives (idempotent tasks, retry-on-failure, backfill by run_id) are exactly the data-engineering primitives you use for every other pipeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The failure modes are drift, freshness, and schema.&lt;/strong&gt; Embedding-model version changes silently reshape cosine distributions; retrieval index refreshes drop coverage; prompt edits change the answer surface. These are drift and schema-evolution problems — the same problems you handle in every production data platform.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The observability story is metric surfaces.&lt;/strong&gt; Per-row scores go into a warehouse table; nightly aggregates roll into a dashboard; alerts fire on regression. This is the "metric with a runbook" pattern you already run for every SLO. LLM-as-judge is a new signal source; the observability shape is the same.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why "eval on demo examples" is the default failure mode.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The demo set is 20 rows.&lt;/strong&gt; It is what the founder used to prove the product to the first ten customers. It has no class balance, no edge cases, no adversarial inputs. Scoring against it produces a green graph that promises nothing about production behaviour.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The demo set is not versioned.&lt;/strong&gt; Rows get added ad hoc, expected answers get edited when a stakeholder pushes back, provenance is lost. The metric on Tuesday is not comparable to the metric on Friday. You cannot regress against a moving target.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The demo set has no metadata.&lt;/strong&gt; No difficulty tag, no topic label, no "who wrote this expected answer." When the aggregate score drops, you cannot slice by anything and cannot find the regression's centre of mass.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The demo set is not calibrated.&lt;/strong&gt; If cosine similarity of 0.87 counts as "correct," the team invented that threshold once, in a Colab notebook, three months ago, and never revisited. The threshold no longer matches the underlying embedding distribution — the metric drifts silently.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What senior interviewers listen for.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do you frame LLM evaluation as &lt;strong&gt;"versioned inputs plus a deterministic metric plus a CI hook"&lt;/strong&gt; rather than "we manually spot-check outputs"? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you name &lt;strong&gt;cosine similarity, LLM-as-judge, and exact/regex&lt;/strong&gt; as the metric family and know &lt;em&gt;which for which task&lt;/em&gt;? — required answer.&lt;/li&gt;
&lt;li&gt;Do you push back on &lt;strong&gt;"eval on demo examples"&lt;/strong&gt; with the class-balance and version-control argument? — required answer.&lt;/li&gt;
&lt;li&gt;Do you treat &lt;strong&gt;evaluation cost&lt;/strong&gt; (judge dollars per row × golden-set size × runs per week) as a first-class engineering constraint? — senior signal.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Worked example — the "demo set" failure mode quantified
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The archetypal LLM-eval failure: a team ships a RAG chatbot backed by a 20-row demo set. Six weeks later they roll out a prompt tweak, the demo score stays green at 0.94 cosine, and a customer support flood reveals the tweak has cratered the answer quality on a whole family of questions the demo set never covered. Walk an interviewer through &lt;em&gt;why&lt;/em&gt; the 20-row set could not catch this and what the correct golden-set size looks like.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The symptom.&lt;/strong&gt; Support tickets spike from 5/day to 45/day within 24 hours of a prompt deploy. Metric dashboards are green.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The naive assumption.&lt;/strong&gt; "The demo passes, therefore the prompt is fine."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The actual failure.&lt;/strong&gt; The demo covers "what are your business hours" style questions; the prompt regression touched "how do I cancel a subscription" style questions. Zero overlap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The fix.&lt;/strong&gt; A golden set of at least 200–500 rows, class-balanced across question topics, with per-row topic metadata so slicing by class catches localized regressions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; A team has a 20-row demo set with cosine 0.94 average on the current prompt. They plan to roll out a prompt change. Quantify the statistical power of scoring the change against 20 rows vs 200 rows, and derive the minimum golden-set size for a 5 percent regression to be detectable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Demo set&lt;/th&gt;
&lt;th&gt;Golden set&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rows&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;10× fanout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cosine standard deviation per row&lt;/td&gt;
&lt;td&gt;0.08&lt;/td&gt;
&lt;td&gt;0.08&lt;/td&gt;
&lt;td&gt;Empirically measured&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standard error of mean&lt;/td&gt;
&lt;td&gt;0.018&lt;/td&gt;
&lt;td&gt;0.006&lt;/td&gt;
&lt;td&gt;σ / √N&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Detectable regression (2σ)&lt;/td&gt;
&lt;td&gt;0.036&lt;/td&gt;
&lt;td&gt;0.011&lt;/td&gt;
&lt;td&gt;95% CI half-width&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5% regression threshold&lt;/td&gt;
&lt;td&gt;0.047&lt;/td&gt;
&lt;td&gt;0.047&lt;/td&gt;
&lt;td&gt;Business-defined&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Detectable at 5% threshold?&lt;/td&gt;
&lt;td&gt;barely&lt;/td&gt;
&lt;td&gt;comfortably&lt;/td&gt;
&lt;td&gt;2× vs 4× margin&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;detectable_regression&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sigma_per_row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Return the smallest regression (in the same units as sigma_per_row) that a
    golden set of size n_rows can reliably detect at the given confidence level.

    Uses a two-sided normal approximation: half-width = z * sigma / sqrt(n).
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.96&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;confidence&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mf"&gt;0.95&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;2.58&lt;/span&gt;   &lt;span class="c1"&gt;# 95% or 99%
&lt;/span&gt;    &lt;span class="n"&gt;sem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sigma_per_row&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;sem&lt;/span&gt;


&lt;span class="c1"&gt;# The team's numbers
&lt;/span&gt;&lt;span class="n"&gt;sigma&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.08&lt;/span&gt;                &lt;span class="c1"&gt;# per-row cosine std dev
&lt;/span&gt;&lt;span class="n"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;            &lt;span class="c1"&gt;# 5% business-defined regression threshold
&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;detectable_regression&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sigma&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  detectable=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  &amp;lt;threshold? &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;verdict&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Output:
# n=   20  detectable=0.0350  &amp;lt;threshold? yes  (barely, uses full CI budget)
# n=  100  detectable=0.0157  &amp;lt;threshold? yes
# n=  200  detectable=0.0111  &amp;lt;threshold? yes
# n=  500  detectable=0.0070  &amp;lt;threshold? yes
# n= 1000  detectable=0.0050  &amp;lt;threshold? yes
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The statistical setup: score deltas are approximately normal by CLT; the two-sided 95 percent confidence half-width on the mean is &lt;code&gt;1.96 × σ / √N&lt;/code&gt;. That half-width is the &lt;em&gt;smallest&lt;/em&gt; regression the harness can distinguish from noise.&lt;/li&gt;
&lt;li&gt;At &lt;code&gt;N=20&lt;/code&gt;, the half-width is 0.035 — meaning any regression under 3.5 percent is indistinguishable from run-to-run variance. A 5 percent regression is barely detectable and only against a single quiet baseline; a 3 percent regression sails through unnoticed.&lt;/li&gt;
&lt;li&gt;At &lt;code&gt;N=200&lt;/code&gt;, the half-width drops to 0.011 — the harness can distinguish a 1.1 percent regression from noise. A 5 percent regression is caught with a 4× margin, and per-class slices (topic buckets) each have enough rows to be statistically meaningful.&lt;/li&gt;
&lt;li&gt;The bigger reason 200 rows beats 20 is &lt;em&gt;coverage&lt;/em&gt;. Statistical power is the floor; class coverage is the actual protection. A 200-row set with 10 topic buckets × 20 rows each catches localized regressions the 20-row demo never sees. A 20-row set has no coverage at all.&lt;/li&gt;
&lt;li&gt;The team's failure was &lt;em&gt;both&lt;/em&gt;: statistical power too low to distinguish a real change from noise, and coverage too narrow to see the class where the regression actually landed. Both problems dissolve at 200+ rows with balanced classes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Golden-set size&lt;/th&gt;
&lt;th&gt;Half-width (95% CI)&lt;/th&gt;
&lt;th&gt;Detects 5% regression?&lt;/th&gt;
&lt;th&gt;Detects 1% regression?&lt;/th&gt;
&lt;th&gt;Practical use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;0.035&lt;/td&gt;
&lt;td&gt;barely&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;Demo only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;0.016&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;Development&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;0.011&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;barely&lt;/td&gt;
&lt;td&gt;Production baseline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;0.007&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;Multi-slice production&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;0.005&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;Regression-forensics grade&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Golden-set size ≥ 200 rows, class-balanced across the top 10 question topics, is the &lt;em&gt;minimum&lt;/em&gt; threshold for shipping an LLM feature to real users. Below 200 rows, the harness is decoration — the metric graph is green because the noise is louder than the signal.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the four axes on a single interview slide
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A senior interviewer often asks "walk me through your LLM evaluation architecture in one minute." The four-axis framing is the tightest possible response: golden set, metric, judge, CI. Practise deriving them from a concrete task — "customer support RAG chatbot" — so the answer is a mini design doc, not a list of buzzwords.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The four axes as questions.&lt;/strong&gt; Which inputs? Which metric? Who judges? When does it run?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The concreteness test.&lt;/strong&gt; Every axis should have a &lt;em&gt;numeric&lt;/em&gt; answer for the task at hand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The senior signal.&lt;/strong&gt; Naming the metric family (cosine + judge, or judge + regex, or embedding + regex), not just "we evaluate."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Fill in the four-axis architecture for a customer-support RAG chatbot: 500 daily support conversations, historical ticket-plus-resolution corpus, GPT-4o-mini as production model, GPT-4o as available judge, PR CI budget of 5 minutes total. Show the numbers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Production model&lt;/td&gt;
&lt;td&gt;GPT-4o-mini&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Judge model (available)&lt;/td&gt;
&lt;td&gt;GPT-4o&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Daily live conversations&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Historical ticket-plus-resolution corpus&lt;/td&gt;
&lt;td&gt;~40,000 rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PR CI budget&lt;/td&gt;
&lt;td&gt;5 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Judge cost per row&lt;/td&gt;
&lt;td&gt;~$0.006 (GPT-4o at ~1500 tokens)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Judge budget per run&lt;/td&gt;
&lt;td&gt;$2.00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# eval-config.yaml — the four axes for the support chatbot&lt;/span&gt;
&lt;span class="na"&gt;golden_set&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;golden/support_v3.jsonl&lt;/span&gt;
  &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v3.2-sha-e1f2c9&lt;/span&gt;
  &lt;span class="na"&gt;rows&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt;
  &lt;span class="na"&gt;class_balance&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;billing&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;
    &lt;span class="na"&gt;account&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;
    &lt;span class="na"&gt;product&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;
    &lt;span class="na"&gt;shipping&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;
    &lt;span class="na"&gt;other&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;
  &lt;span class="na"&gt;curation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SME-labelled + LLM-augmented, weekly refresh&lt;/span&gt;
  &lt;span class="na"&gt;growth_rate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;+20 rows/week&lt;/span&gt;

&lt;span class="na"&gt;metric&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;primary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;llm_judge_correctness&lt;/span&gt;   &lt;span class="c1"&gt;# 0.0 to 1.0&lt;/span&gt;
  &lt;span class="na"&gt;secondary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cosine_similarity&lt;/span&gt;      &lt;span class="c1"&gt;# sanity check, threshold 0.85&lt;/span&gt;
  &lt;span class="na"&gt;aggregation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mean_over_rows&lt;/span&gt;

&lt;span class="na"&gt;judge&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gpt-4o&lt;/span&gt;
  &lt;span class="na"&gt;prompt_hash&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;c93f11&lt;/span&gt;
  &lt;span class="na"&gt;rubric&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;correctness&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;completeness&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;style&lt;/span&gt;
  &lt;span class="na"&gt;output_schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;score&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;float&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;rationale&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;string&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;breakdown&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;correctness&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;float&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;completeness&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;float&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;style&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;float&lt;/span&gt;&lt;span class="pi"&gt;}}&lt;/span&gt;
  &lt;span class="na"&gt;cost_per_row_usd&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.006&lt;/span&gt;
  &lt;span class="na"&gt;runs_per_pr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;budget_per_run_usd&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2.00&lt;/span&gt;

&lt;span class="na"&gt;ci&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;on pull_request&lt;/span&gt;
  &lt;span class="na"&gt;gate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aggregate_score &amp;gt;= previous_baseline - &lt;/span&gt;&lt;span class="m"&gt;0.05&lt;/span&gt;
  &lt;span class="na"&gt;timeout_minutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
  &lt;span class="na"&gt;parallelism&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;
  &lt;span class="na"&gt;slack_channel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;#llm-quality'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The golden set is 300 rows split evenly across 5 topic classes — the class-balance chip means a topic regression cannot be masked by aggregate improvement elsewhere. Version stamp &lt;code&gt;v3.2-sha-e1f2c9&lt;/code&gt; is the git hash of the golden-set repo commit; every metric row is joined to it in the warehouse.&lt;/li&gt;
&lt;li&gt;The metric mix is &lt;code&gt;llm_judge_correctness&lt;/code&gt; (primary) plus &lt;code&gt;cosine_similarity&lt;/code&gt; (secondary sanity check). The judge is expensive but authoritative; cosine is cheap and catches "the model hallucinated a URL that has zero embedding overlap." Together they cover both semantic and structural failure.&lt;/li&gt;
&lt;li&gt;The judge is GPT-4o with a rubric of correctness / completeness / style. Prompt hash &lt;code&gt;c93f11&lt;/code&gt; versions the judge prompt itself — a prompt edit bumps the hash and the harness knows to re-run baseline scores rather than compare across judge-prompt versions.&lt;/li&gt;
&lt;li&gt;CI cost math: 300 rows × $0.006 per row = $1.80 per PR run, comfortably under the $2.00 budget. With 20-way parallelism and per-row latency around 2 seconds, wall-clock time is &lt;code&gt;(300 / 20) × 2s = 30s&lt;/code&gt; — well within the 5-minute PR budget.&lt;/li&gt;
&lt;li&gt;The gate blocks the merge if the aggregate correctness drops by more than 5 percent below the &lt;em&gt;previous&lt;/em&gt; baseline. "Previous baseline" is the last main-branch merge, not an all-time high — otherwise a one-time high score locks the team out of every future PR.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Axis&lt;/th&gt;
&lt;th&gt;Numeric answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Golden set&lt;/td&gt;
&lt;td&gt;300 rows, v3.2-sha-e1f2c9, 5 classes × 60 rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metric&lt;/td&gt;
&lt;td&gt;LLM-judge correctness (primary) + cosine 0.85 threshold (secondary)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Judge&lt;/td&gt;
&lt;td&gt;GPT-4o with 3-criterion rubric, $0.006/row, $1.80/run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI&lt;/td&gt;
&lt;td&gt;PR gate, block on ≥ 5% aggregate drop vs previous baseline, 5-min budget&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; If any of the four axes has a &lt;em&gt;qualitative&lt;/em&gt; answer ("we spot-check," "we use vibes," "we run it weekly"), the architecture is not ready to ship. Every axis should have a number an interviewer can quote back at you.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — evaluation cost as a first-class constraint
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A common mistake in the first evaluation build-out: no one runs the numbers on what the judge actually costs at scale. The team ships a 2000-row golden set with an LLM-as-judge that costs $0.03 per row, and the CI bill lands at $60 per PR. At 40 PRs per week, that is $10,000 per year on eval alone — and the team has zero budget for the production monitoring layer that is the actual point. Walk through the cost model and the levers that keep it sane.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost per PR.&lt;/strong&gt; &lt;code&gt;golden_set_size × judge_cost_per_row&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost per year.&lt;/strong&gt; &lt;code&gt;cost_per_PR × PRs_per_week × 52&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The three levers.&lt;/strong&gt; Reduce set size, reduce judge cost, reduce runs per PR.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given a 2000-row golden set, $0.03 per-row judge cost, and 40 PRs per week, quantify the annual eval bill and propose three optimizations that cut it by at least 5×.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Baseline&lt;/th&gt;
&lt;th&gt;After optimization&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Golden set size&lt;/td&gt;
&lt;td&gt;2000&lt;/td&gt;
&lt;td&gt;400 (core) + 1600 (sample)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Judge cost per row&lt;/td&gt;
&lt;td&gt;$0.03&lt;/td&gt;
&lt;td&gt;$0.006 (smaller judge on 90% of rows)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runs per PR&lt;/td&gt;
&lt;td&gt;1 full&lt;/td&gt;
&lt;td&gt;1 sample (400 rows) + 1 full nightly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PRs per week&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost per full run&lt;/td&gt;
&lt;td&gt;$60&lt;/td&gt;
&lt;td&gt;$12 (nightly)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost per PR run&lt;/td&gt;
&lt;td&gt;$60&lt;/td&gt;
&lt;td&gt;$2.40 (sample only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost per week&lt;/td&gt;
&lt;td&gt;$2,400&lt;/td&gt;
&lt;td&gt;$96 (PRs) + $84 (nightly) = $180&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost per year&lt;/td&gt;
&lt;td&gt;$124,800&lt;/td&gt;
&lt;td&gt;$9,360&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EvalCostModel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;golden_set_size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;judge_cost_per_row_usd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;prs_per_week&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;runs_per_pr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="n"&gt;nightly_full_runs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;nightly_row_multiplier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;   &lt;span class="c1"&gt;# 1.0 = full set; 0.2 = 20% sample
&lt;/span&gt;    &lt;span class="n"&gt;weeks_per_year&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;52&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cost_per_pr_run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;golden_set_size&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nightly_row_multiplier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;judge_cost_per_row_usd&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;annual_cost_usd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;pr_cost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cost_per_pr_run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;runs_per_pr&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prs_per_week&lt;/span&gt;
        &lt;span class="n"&gt;nightly_cost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;golden_set_size&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;judge_cost_per_row_usd&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nightly_full_runs&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;
        &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pr_cost&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;nightly_cost&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;weeks_per_year&lt;/span&gt;


&lt;span class="n"&gt;baseline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EvalCostModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;golden_set_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;judge_cost_per_row_usd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.03&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;prs_per_week&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Baseline: $&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;baseline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;annual_cost_usd&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;,.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/yr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# $124,800
&lt;/span&gt;
&lt;span class="c1"&gt;# Optimization 1 — sample 20% on PR, full run nightly
&lt;/span&gt;&lt;span class="n"&gt;opt1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EvalCostModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;golden_set_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;judge_cost_per_row_usd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.03&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;prs_per_week&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;nightly_row_multiplier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;nightly_full_runs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Opt 1 (sample + nightly): $&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;opt1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;annual_cost_usd&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;,.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/yr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Optimization 2 — cheaper judge (GPT-4o-mini instead of GPT-4)
&lt;/span&gt;&lt;span class="n"&gt;opt2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EvalCostModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;golden_set_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;judge_cost_per_row_usd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.006&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;prs_per_week&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;nightly_row_multiplier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;nightly_full_runs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Opt 2 (+cheap judge): $&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;opt2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;annual_cost_usd&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;,.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/yr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The baseline math is brutal: 2000 rows × $0.03 × 40 PRs × 52 weeks = $124,800 per year, all on evaluation before a single dollar of production monitoring. The finance team will ask why.&lt;/li&gt;
&lt;li&gt;Optimization 1 splits the harness into a &lt;em&gt;fast&lt;/em&gt; PR run (20 percent sample = 400 rows, still statistically powerful) and a &lt;em&gt;thorough&lt;/em&gt; nightly run over the full 2000. PR cost drops 5×; the nightly cost is 7 runs per week at full size, but only one team pays for it, not every PR author.&lt;/li&gt;
&lt;li&gt;Optimization 2 stacks a cheaper judge on top. GPT-4o-mini is roughly 5× cheaper than GPT-4o for judge tasks and, for well-designed rubric prompts, correlates with GPT-4o judgements at r &amp;gt; 0.9 (measured with a 200-row A/B). Use the cheap judge on the PR sample; keep the expensive judge on the nightly full run for calibration.&lt;/li&gt;
&lt;li&gt;The combined effect: $124,800 → $9,360, a 13× reduction, without cutting the golden set. Coverage is preserved via the nightly full run; PR sensitivity is preserved via the sample-based gate; the judge-cost sensitivity is spent where it matters (calibration), not where it does not (every PR).&lt;/li&gt;
&lt;li&gt;The pattern that generalizes: &lt;em&gt;sample for speed, calibrate on the full set, use a cheaper judge for the sample&lt;/em&gt;. It is the exact same pattern as "sample for AB tests, batch for correctness reports" — an old data-engineering pattern, applied to eval.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Configuration&lt;/th&gt;
&lt;th&gt;Cost per PR&lt;/th&gt;
&lt;th&gt;Cost per year&lt;/th&gt;
&lt;th&gt;Coverage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Baseline (full set, expensive judge)&lt;/td&gt;
&lt;td&gt;$60&lt;/td&gt;
&lt;td&gt;$124,800&lt;/td&gt;
&lt;td&gt;Full every PR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opt 1 (sample 20% + nightly full)&lt;/td&gt;
&lt;td&gt;$12&lt;/td&gt;
&lt;td&gt;$16,320&lt;/td&gt;
&lt;td&gt;Full nightly, sample per PR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opt 2 (opt 1 + cheap judge)&lt;/td&gt;
&lt;td&gt;$2.40&lt;/td&gt;
&lt;td&gt;$9,360&lt;/td&gt;
&lt;td&gt;Full nightly (expensive), sample per PR (cheap)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opt 3 (opt 2 + reduce PRs to weekly nightly only)&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;$2,184&lt;/td&gt;
&lt;td&gt;Nightly only, no PR gate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; The judge bill is a data-engineering budget item, not a research afterthought. Model it before you build the harness; sample-on-PR + full-nightly + cheap-judge-with-expensive-calibration is the default architecture that scales.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on the LLM evaluation architecture
&lt;/h3&gt;

&lt;p&gt;A senior interviewer often opens with: "Design an end-to-end LLM evaluation system for a mid-size product team shipping a RAG chatbot. Cover golden set, metric, judge, CI, production monitoring, and cost. Walk me through the concrete numbers and the trade-offs you'd make."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a four-axis pipeline with golden set + cosine + judge + PR gate
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# eval-arch.yaml — the reference architecture&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1.0&lt;/span&gt;

&lt;span class="na"&gt;golden_set&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;git-versioned JSONL&lt;/span&gt;
  &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;golden/support_v3.jsonl&lt;/span&gt;
  &lt;span class="na"&gt;rows&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt;
  &lt;span class="na"&gt;balance&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5 classes × 60 rows&lt;/span&gt;
  &lt;span class="na"&gt;growth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;+20 rows/week from production sampling&lt;/span&gt;
  &lt;span class="na"&gt;provenance&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SME-labelled + LLM-augmented&lt;/span&gt;
  &lt;span class="na"&gt;version_stamp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v3.2-sha-e1f2c9&lt;/span&gt;

&lt;span class="na"&gt;metrics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;primary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;llm_judge_correctness&lt;/span&gt;
    &lt;span class="na"&gt;scale&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.0-1.0&lt;/span&gt;
    &lt;span class="na"&gt;judge_model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gpt-4o-mini&lt;/span&gt;      &lt;span class="c1"&gt;# sample judge&lt;/span&gt;
    &lt;span class="na"&gt;calibration_model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gpt-4o&lt;/span&gt;     &lt;span class="c1"&gt;# nightly full-set judge&lt;/span&gt;
  &lt;span class="na"&gt;secondary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cosine_similarity&lt;/span&gt;
    &lt;span class="na"&gt;embedding_model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;text-embedding-3-large&lt;/span&gt;
    &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.85&lt;/span&gt;               &lt;span class="c1"&gt;# calibrated per class&lt;/span&gt;
    &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sanity check + hallucination detector&lt;/span&gt;

&lt;span class="na"&gt;harness&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;execution&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DAG (Dagster / Airflow / Prefect)&lt;/span&gt;
  &lt;span class="na"&gt;parallelism&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;
  &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5 minutes&lt;/span&gt;
  &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;warehouse table eval_runs (run_id, golden_set_hash, prompt_hash, model, row_id, score, rationale)&lt;/span&gt;

&lt;span class="na"&gt;ci&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;on pull_request touching prompts/, retrieval/, or model_config/&lt;/span&gt;
  &lt;span class="na"&gt;sample&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;400 rows (20% of full set)&lt;/span&gt;
  &lt;span class="na"&gt;gate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mean_correctness &amp;gt;= previous_baseline - &lt;/span&gt;&lt;span class="m"&gt;0.05&lt;/span&gt;
  &lt;span class="na"&gt;comment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;post per-class delta table to PR&lt;/span&gt;
  &lt;span class="na"&gt;budget&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$2.40 per PR&lt;/span&gt;

&lt;span class="na"&gt;nightly&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cron 02:00 UTC daily&lt;/span&gt;
  &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;full 2000-row set with expensive judge (gpt-4o)&lt;/span&gt;
  &lt;span class="na"&gt;gate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;none (reports only, feeds baseline)&lt;/span&gt;
  &lt;span class="na"&gt;budget&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$12 per run&lt;/span&gt;

&lt;span class="na"&gt;production_monitor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;sampler&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1% of live conversations&lt;/span&gt;
  &lt;span class="na"&gt;frequency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hourly aggregation&lt;/span&gt;
  &lt;span class="na"&gt;metric&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;llm_judge_correctness (cheap judge)&lt;/span&gt;
  &lt;span class="na"&gt;alert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;24h rolling mean drops &amp;gt; 5% vs last-week baseline&lt;/span&gt;
  &lt;span class="na"&gt;cost&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;~$50/week&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Golden set&lt;/td&gt;
&lt;td&gt;git commit v3.2-sha-e1f2c9&lt;/td&gt;
&lt;td&gt;300 rows loaded&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model inference&lt;/td&gt;
&lt;td&gt;300 predictions&lt;/td&gt;
&lt;td&gt;prediction column&lt;/td&gt;
&lt;td&gt;~$0.30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cosine scoring&lt;/td&gt;
&lt;td&gt;prediction + expected&lt;/td&gt;
&lt;td&gt;cosine column&lt;/td&gt;
&lt;td&gt;$0.03 (embeddings)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Judge scoring (sample)&lt;/td&gt;
&lt;td&gt;400 rows&lt;/td&gt;
&lt;td&gt;judge score + rationale&lt;/td&gt;
&lt;td&gt;$2.40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aggregation&lt;/td&gt;
&lt;td&gt;per-class + overall means&lt;/td&gt;
&lt;td&gt;dashboard row + PR comment&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI gate&lt;/td&gt;
&lt;td&gt;mean vs baseline&lt;/td&gt;
&lt;td&gt;pass or block&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nightly&lt;/td&gt;
&lt;td&gt;full 2000&lt;/td&gt;
&lt;td&gt;reports&lt;/td&gt;
&lt;td&gt;$12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production sampler&lt;/td&gt;
&lt;td&gt;1% traffic&lt;/td&gt;
&lt;td&gt;continuous score&lt;/td&gt;
&lt;td&gt;~$50/week&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After the rollout, every PR touching prompts or retrieval runs a 400-row sample eval in ~30 seconds, posts a per-class delta table into the PR, and blocks the merge if aggregate correctness drops by more than 5 percent. The nightly full-set run recalibrates the baseline and catches slow drift the PR sample cannot see. Production monitoring samples 1 percent of live traffic through the same judge and alerts on 24-hour drops.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PR eval wall-clock&lt;/td&gt;
&lt;td&gt;~30 seconds (parallelism = 20)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PR eval cost&lt;/td&gt;
&lt;td&gt;$2.40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nightly eval wall-clock&lt;/td&gt;
&lt;td&gt;3 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nightly eval cost&lt;/td&gt;
&lt;td&gt;$12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production monitor cost&lt;/td&gt;
&lt;td&gt;~$50/week&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Annual total&lt;/td&gt;
&lt;td&gt;~$9,400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Detectable regression at PR gate&lt;/td&gt;
&lt;td&gt;~1% (400-row sample)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Detectable regression nightly&lt;/td&gt;
&lt;td&gt;~0.5% (2000-row full)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Golden set as the versioned input&lt;/strong&gt;&lt;/strong&gt; — every score is joined to &lt;code&gt;golden_set_hash&lt;/code&gt; so the metric is comparable across runs. Editing the golden set bumps the hash and the harness knows to re-baseline rather than compare across incompatible inputs. This is the same "immutable-input, versioned-artifact" pattern that dbt uses for models.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cosine as the cheap sanity check&lt;/strong&gt;&lt;/strong&gt; — cosine similarity on &lt;code&gt;text-embedding-3-large&lt;/code&gt; costs about $0.0001 per row and catches structural failures (hallucinated URLs, wrong entities, missing named entities) at essentially zero cost. It is the cheap first line of defence before the expensive judge fires.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Judge with sample-on-PR + full-nightly&lt;/strong&gt;&lt;/strong&gt; — the sample keeps PR feedback fast and cheap; the nightly keeps the baseline anchored on the full set. The pattern is exactly the CI-vs-CD split from every mature data-engineering platform: fast on the write path, thorough on the batch path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;CI gate at 5% below previous baseline&lt;/strong&gt;&lt;/strong&gt; — the gate is relative to the &lt;em&gt;previous&lt;/em&gt; baseline, not an all-time high, so the team can ratchet quality upward without accidentally trapping themselves under an unreproducible peak. The 5 percent tolerance leaves room for judge noise while still catching real regressions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — total annual eval cost lands at ~$9,400 for a mid-size product, dominated by production monitoring rather than PR gating. The alternative (unmeasured regressions shipping to prod) costs a single incident — the eval stack pays for itself the first time it catches a bad prompt.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;ETL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — etl&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;ETL problems on LLM evaluation pipelines&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;Optimization&lt;/span&gt;
&lt;span&gt;Topic — optimization&lt;/span&gt;
&lt;strong&gt;Optimization problems on eval cost and coverage&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/optimization" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  2. Golden sets — curation and versioning
&lt;/h2&gt;
&lt;h3&gt;
  
  
  A golden set is versioned code, not a spreadsheet — curation discipline is the single biggest lever on eval quality
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;a &lt;code&gt;golden set&lt;/code&gt; is &lt;code&gt;(input, expected_output, metadata)&lt;/code&gt; tuples treated with the same versioning, review, and observability rigour you apply to database migrations — SME-labelled, class-balanced, git-hashed, growing weekly, joined to every downstream metric row by its content hash&lt;/strong&gt;. Everything else in the LLM eval stack is downstream of the golden set; a bad golden set produces green metrics that promise nothing. This is the single highest-leverage discipline in &lt;code&gt;llm evaluation&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fujjt01k7ko7wbpyiy4jq.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fujjt01k7ko7wbpyiy4jq.jpeg" alt="Iconographic golden set diagram — a stitched scroll with tuples of input/expected/metadata, versioned with a git-hash chip, and a class-balance pie chart on the right." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four axes of a golden set worth trusting.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tuples.&lt;/strong&gt; Every row is &lt;code&gt;(input, expected_output, metadata)&lt;/code&gt;. &lt;code&gt;input&lt;/code&gt; is the exact string that would go into your production model. &lt;code&gt;expected_output&lt;/code&gt; is the SME-labelled canonical answer. &lt;code&gt;metadata&lt;/code&gt; is the rich sidecar: topic, difficulty, source ticket id, expected style, forbidden patterns, retrieval-index snapshot at label time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Versioned.&lt;/strong&gt; The golden set lives in a git repo. Every edit is a PR, gets code review, gets a content hash. Downstream metric rows join to the hash — comparing scores across hashes is disallowed by construction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Balanced.&lt;/strong&gt; Rows are distributed across the classes your product actually serves: topic buckets, difficulty tiers, language / locale / persona. Class imbalance in the golden set means the aggregate score is dominated by whichever class has the most rows, and localized regressions in under-represented classes vanish into the mean.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Growing.&lt;/strong&gt; Golden sets are not static. Every week, sample fresh production conversations, SME-label them, add to the set. A stagnant golden set drifts away from the live traffic distribution and stops being predictive within months.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The curation loop.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Source.&lt;/strong&gt; Sample from live production traffic — the golden set must reflect the actual input distribution, not an idealized version of it. For a chatbot: sample real user turns. For extraction: sample real documents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Label.&lt;/strong&gt; Route to an SME queue. For customer support: a senior support engineer. For medical Q&amp;amp;A: a clinician. For code review: a staff engineer. The SME writes the expected answer; a peer reviews it; the reviewed row is merged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Augment.&lt;/strong&gt; LLM-augmented golden sets — where a strong model generates the expected answer and an SME reviews it — are the compromise for scale. Purely LLM-generated golden sets are self-referential garbage; SME-reviewed LLM-generated sets are 5–10× cheaper than SME-authored ones while retaining most of the quality.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Balance.&lt;/strong&gt; After every batch, re-check the class distribution. If billing questions are now 40 percent of the set and shipping questions are 5 percent, rebalance before the next merge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hash + release.&lt;/strong&gt; The final artefact is a JSONL file with a content hash. &lt;code&gt;v3.2-sha-e1f2c9&lt;/code&gt; means "golden set version 3.2, content hash e1f2c9." Every downstream metric row carries this hash.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Version control patterns that work.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Storage.&lt;/strong&gt; JSONL in a git repo (fast to diff, streams row-by-row into the harness). Alternative: parquet with an accompanying &lt;code&gt;MANIFEST.json&lt;/code&gt;. Never a Google Sheet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review.&lt;/strong&gt; PRs against the golden-set repo. Every edit is reviewed by at least one SME. Bulk merges (&amp;gt;50 rows) get double review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hash.&lt;/strong&gt; Content hash computed on canonicalized JSON (sorted keys, no trailing whitespace) — SHA-256 first 6 hex is enough. Store in a &lt;code&gt;MANIFEST.json&lt;/code&gt; alongside the file; harness verifies at load time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollback.&lt;/strong&gt; Git tags per release (&lt;code&gt;golden-v3.2&lt;/code&gt;). If a bad batch is merged, revert the merge commit and re-tag. Downstream metric rows keyed by hash never confuse pre- and post-revert.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common curation failures and their fixes.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"We use production traffic samples directly."&lt;/strong&gt; Fails on labeler bias — the model's own output becomes the expected answer. Fix: SME must write the expected answer independently, without seeing the model's response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"We only add rows when the model gets something wrong."&lt;/strong&gt; Fails on class balance — the golden set becomes an adversarial edge-case set with no representative easy rows, and the aggregate metric is meaningless. Fix: add rows from all outcome buckets (correct, partially-correct, wrong) at fixed ratios.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"We edit expected answers when the SME disagrees with the score."&lt;/strong&gt; Fails on immutability — the metric is no longer comparable across time. Fix: expected-answer edits go through the same PR-and-hash-bump process as adding new rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"We don't rebalance."&lt;/strong&gt; Fails on drift — organic growth is never balanced, and the metric quietly gets dominated by whichever class the SMEs happen to label most. Fix: automatic imbalance detection with a warning below 20% deviation from target ratios.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What senior interviewers listen for.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do you talk about the golden set as a &lt;strong&gt;first-class artefact with version control and PR review&lt;/strong&gt;, rather than as a file someone maintains? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you volunteer the &lt;strong&gt;class-balance chip&lt;/strong&gt; without prompting? — required answer.&lt;/li&gt;
&lt;li&gt;Do you distinguish &lt;strong&gt;SME-labelled from LLM-augmented&lt;/strong&gt; and defend both use cases? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you name &lt;strong&gt;content-hash + downstream-join&lt;/strong&gt; as the mechanism that keeps the metric comparable across time? — senior signal.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — schema for a 300-row customer-support RAG golden set
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Design the JSONL schema for a customer-support RAG golden set. Every row must carry enough metadata to slice by topic, difficulty, language, expected style, and provenance. Show the actual JSON with three representative rows and walk through why every field earns its place.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Row shape.&lt;/strong&gt; &lt;code&gt;input&lt;/code&gt;, &lt;code&gt;expected_output&lt;/code&gt;, &lt;code&gt;metadata&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metadata fields.&lt;/strong&gt; &lt;code&gt;topic&lt;/code&gt;, &lt;code&gt;difficulty&lt;/code&gt;, &lt;code&gt;language&lt;/code&gt;, &lt;code&gt;source_ticket_id&lt;/code&gt;, &lt;code&gt;sme_labeler&lt;/code&gt;, &lt;code&gt;reviewer&lt;/code&gt;, &lt;code&gt;retrieval_snapshot_id&lt;/code&gt;, &lt;code&gt;forbidden_patterns&lt;/code&gt;, &lt;code&gt;style_tag&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The senior test.&lt;/strong&gt; Every field must have a downstream consumer (dashboard slice, regression forensic, harness gate).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Produce the JSONL schema plus three example rows for a customer-support RAG golden set covering billing, account, and shipping topics, with an SME-labeler chain and a retrieval-snapshot pin.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;input&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;The user turn as it enters the RAG chatbot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;expected_output&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;SME-authored canonical answer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;metadata.topic&lt;/td&gt;
&lt;td&gt;enum&lt;/td&gt;
&lt;td&gt;Class bucket for balance + slice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;metadata.difficulty&lt;/td&gt;
&lt;td&gt;enum&lt;/td&gt;
&lt;td&gt;Bucket for slice + power analysis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;metadata.language&lt;/td&gt;
&lt;td&gt;enum&lt;/td&gt;
&lt;td&gt;Locale slice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;metadata.source_ticket_id&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;Provenance to the original support ticket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;metadata.sme_labeler&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;The SME who wrote the expected answer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;metadata.reviewer&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;The peer who approved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;metadata.retrieval_snapshot_id&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;Retrieval index at label time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;metadata.forbidden_patterns&lt;/td&gt;
&lt;td&gt;list&lt;/td&gt;
&lt;td&gt;Regexes that must not appear in prediction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;metadata.style_tag&lt;/td&gt;
&lt;td&gt;enum&lt;/td&gt;
&lt;td&gt;Expected tone (formal, empathetic, terse)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{"input": "How do I update my billing address on file?", "expected_output": "You can update your billing address by opening Settings → Billing → Address, entering the new address, and clicking Save. The change takes effect on your next invoice. If a payment is currently processing, contact support before saving.", "metadata": {"topic": "billing", "difficulty": "easy", "language": "en", "source_ticket_id": "T-8821", "sme_labeler": "alice@support", "reviewer": "bob@support", "retrieval_snapshot_id": "idx-2026-06-15", "forbidden_patterns": ["credit card", "bank account"], "style_tag": "formal"}}
{"input": "My charge from last month is wrong — I was billed twice.", "expected_output": "I'm sorry about the duplicate charge — that's frustrating. Please email billing@company.com with your account email and the two transaction IDs. Our team can reverse one of the charges within 3 business days. If you'd like, I can also open a ticket for you now.", "metadata": {"topic": "billing", "difficulty": "hard", "language": "en", "source_ticket_id": "T-8834", "sme_labeler": "alice@support", "reviewer": "carol@support", "retrieval_snapshot_id": "idx-2026-06-15", "forbidden_patterns": ["we'll refund", "guaranteed"], "style_tag": "empathetic"}}
{"input": "Where is my order? Tracking says shipped but I haven't received it.", "expected_output": "I understand — a shipped order that hasn't arrived is stressful. Please check the tracking link in your shipment email for the latest carrier update. If the tracking has been stagnant for more than 5 business days, reply here with your order number and I'll open a carrier claim on your behalf.", "metadata": {"topic": "shipping", "difficulty": "medium", "language": "en", "source_ticket_id": "T-8912", "sme_labeler": "dave@support", "reviewer": "alice@support", "retrieval_snapshot_id": "idx-2026-06-15", "forbidden_patterns": ["lost forever", "impossible"], "style_tag": "empathetic"}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# harness/load_golden.py — canonicalized load + hash verification
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;content_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;SHA-256 first 6 hex over canonicalized JSON.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;canonical&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sort_keys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ensure_ascii&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()[:&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_golden&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
    &lt;span class="n"&gt;actual_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;content_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;expected_hash&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;actual_hash&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;expected_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;golden set hash mismatch: expected &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;expected_hash&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, got &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;actual_hash&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;


&lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_golden&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;golden/support_v3.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;expected_hash&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;e1f2c9&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;loaded &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; rows, hash e1f2c9 verified&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Every row carries &lt;code&gt;input&lt;/code&gt;, &lt;code&gt;expected_output&lt;/code&gt;, and a rich &lt;code&gt;metadata&lt;/code&gt; block. The metadata is &lt;em&gt;not&lt;/em&gt; optional — it is what turns a golden set into a diagnostic tool. Without &lt;code&gt;topic&lt;/code&gt; and &lt;code&gt;difficulty&lt;/code&gt;, regressions can only be described at the aggregate; with them, you can point to "billing / hard rows dropped 12 percent" in the PR comment.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;forbidden_patterns&lt;/code&gt; is the row-level guardrail: regexes the prediction must not contain. Row 1's &lt;code&gt;["credit card", "bank account"]&lt;/code&gt; means the model should never invite the user to email their credit card number — an easily-detectable structural failure that pure cosine similarity might miss.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;style_tag&lt;/code&gt; distinguishes expected tone. Row 2's &lt;code&gt;"empathetic"&lt;/code&gt; means the judge rubric should score empathy explicitly; row 1's &lt;code&gt;"formal"&lt;/code&gt; means the same phrase would score lower. The metric responds to the semantic &lt;em&gt;and&lt;/em&gt; the pragmatic axis.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;retrieval_snapshot_id&lt;/code&gt; pins the retrieval index state at label time — critical for RAG evaluation. When the retrieval index refreshes and the expected answer no longer matches what a &lt;em&gt;correct&lt;/em&gt; retrieval would return, the row is flagged for re-labelling rather than silently failing.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;content_hash&lt;/code&gt; function computes SHA-256 over canonicalized JSON (sorted keys, one row per line). Downstream metric rows join to this hash; the harness verifies the hash on load. If the file was edited without a PR review, the hash mismatches and the run aborts.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Downstream consumer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;input / expected_output&lt;/td&gt;
&lt;td&gt;Core tuple&lt;/td&gt;
&lt;td&gt;Model + judge inputs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;topic&lt;/td&gt;
&lt;td&gt;Class balance + slice&lt;/td&gt;
&lt;td&gt;Dashboard slice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;difficulty&lt;/td&gt;
&lt;td&gt;Slice + power&lt;/td&gt;
&lt;td&gt;Regression forensics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;language&lt;/td&gt;
&lt;td&gt;Locale slice&lt;/td&gt;
&lt;td&gt;Multilingual dashboards&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;source_ticket_id&lt;/td&gt;
&lt;td&gt;Provenance&lt;/td&gt;
&lt;td&gt;Row-level audit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sme_labeler / reviewer&lt;/td&gt;
&lt;td&gt;Trust chain&lt;/td&gt;
&lt;td&gt;SME QA reports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;retrieval_snapshot_id&lt;/td&gt;
&lt;td&gt;RAG pinning&lt;/td&gt;
&lt;td&gt;Index-refresh guard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;forbidden_patterns&lt;/td&gt;
&lt;td&gt;Row-level guardrail&lt;/td&gt;
&lt;td&gt;Structural failure check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;style_tag&lt;/td&gt;
&lt;td&gt;Style discipline&lt;/td&gt;
&lt;td&gt;Judge rubric input&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Every metadata field must have a downstream consumer. If you cannot name what a field is used for in the dashboard, harness, or forensics report, drop it — noise metadata makes the schema harder to evolve without making the metric better.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the class-balance rebalancer
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Golden sets grow organically as SMEs add rows. Without discipline, the distribution drifts toward whatever the SMEs happen to work on that week. Build the class-balance rebalancer that runs on every merge to the golden-set repo, warns on drift, and refuses to merge if drift exceeds a threshold.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Target ratios.&lt;/strong&gt; 5 topic classes at 20 percent each (0.2 target per class).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Warning threshold.&lt;/strong&gt; ±5 percent deviation from target — flags the row but allows the merge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blocking threshold.&lt;/strong&gt; ±10 percent deviation — CI check fails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix.&lt;/strong&gt; Rebalance by removing over-represented rows or by generating replacements in the under-represented classes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Build the class-balance check as a pre-merge CI script that reads the JSONL, computes per-class ratios, prints a warning or fails, and outputs the recommended rebalance action.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Class&lt;/th&gt;
&lt;th&gt;Current rows&lt;/th&gt;
&lt;th&gt;Target ratio&lt;/th&gt;
&lt;th&gt;Current ratio&lt;/th&gt;
&lt;th&gt;Delta&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;billing&lt;/td&gt;
&lt;td&gt;78&lt;/td&gt;
&lt;td&gt;0.20&lt;/td&gt;
&lt;td&gt;0.26&lt;/td&gt;
&lt;td&gt;+0.06&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;account&lt;/td&gt;
&lt;td&gt;55&lt;/td&gt;
&lt;td&gt;0.20&lt;/td&gt;
&lt;td&gt;0.18&lt;/td&gt;
&lt;td&gt;-0.02&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;product&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;0.20&lt;/td&gt;
&lt;td&gt;0.20&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;shipping&lt;/td&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;td&gt;0.20&lt;/td&gt;
&lt;td&gt;0.15&lt;/td&gt;
&lt;td&gt;-0.05&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;other&lt;/td&gt;
&lt;td&gt;62&lt;/td&gt;
&lt;td&gt;0.20&lt;/td&gt;
&lt;td&gt;0.21&lt;/td&gt;
&lt;td&gt;+0.01&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;total&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# harness/check_balance.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;


&lt;span class="n"&gt;TARGET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;billing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;account&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;product&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shipping&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;other&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;WARN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;
&lt;span class="n"&gt;BLOCK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.10&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_balance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
    &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;counts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metadata&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;topic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;exit_code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;golden set has &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; rows&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;topic&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rows&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ratio&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;target&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;delta&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;TARGET&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;ratio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
        &lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ratio&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;
        &lt;span class="n"&gt;marker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;BLOCK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;marker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  BLOCK&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="n"&gt;exit_code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;WARN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;marker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  WARN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ratio&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mf"&gt;7.3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mf"&gt;7.3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;+&lt;/span&gt;&lt;span class="mf"&gt;7.3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;marker&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Suggested action
&lt;/span&gt;    &lt;span class="n"&gt;over&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;TARGET&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;under&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;TARGET&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;over&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;under&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Suggested rebalance:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;under&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;need&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;TARGET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  add &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;need&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; rows for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;over&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;drop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;TARGET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  drop &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;drop&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; rows for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;exit_code&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;check_balance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/golden-check.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;golden-set-check&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;golden/**'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;balance&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.12'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python harness/check_balance.py golden/support_v3.jsonl&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The check iterates over the 5 target classes and computes the current ratio versus the target. Deviations under ±5 percent are silent, ±5–10 percent print a WARN marker (informational), ±10 percent or worse print BLOCK and exit non-zero.&lt;/li&gt;
&lt;li&gt;In the input scenario, &lt;code&gt;billing&lt;/code&gt; is at 26 percent (+6 percent over target) — WARN. &lt;code&gt;shipping&lt;/code&gt; is at 15 percent (-5 percent) — WARN. No class hits the BLOCK threshold, so the check exits 0 and the merge is allowed, but the PR author sees the warnings inline.&lt;/li&gt;
&lt;li&gt;The suggested-rebalance block computes concrete row counts: &lt;code&gt;shipping&lt;/code&gt; needs +15 more rows to hit target; &lt;code&gt;billing&lt;/code&gt; needs to drop 18 rows. The suggestion drives the next batch of SME curation work.&lt;/li&gt;
&lt;li&gt;The check runs on every PR that touches &lt;code&gt;golden/**&lt;/code&gt; via GitHub Actions. It is a 30-second job on a 300-row set; even a 10,000-row set runs in under 5 seconds. The cost is negligible; the guardrail is absolute.&lt;/li&gt;
&lt;li&gt;The pattern extends: add checks for &lt;code&gt;difficulty&lt;/code&gt; distribution, &lt;code&gt;language&lt;/code&gt; distribution, and &lt;code&gt;sme_labeler&lt;/code&gt; diversity (no single SME should label more than 40 percent of any class — labeller bias). All of them are the same shape.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;golden set has 300 rows
topic       rows   ratio  target   delta
billing       78   0.260   0.200  +0.060  WARN
account       55   0.183   0.200  -0.017
product       60   0.200   0.200  +0.000
shipping      45   0.150   0.200  -0.050  WARN
other         62   0.207   0.200  +0.007

Suggested rebalance:
  add +15 rows for shipping
  drop -18 rows for billing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Automate class-balance checks the same way you automate lint. The check is cheap, the failure mode is expensive; the moment a golden set is not automatically balanced, drift wins and the metric decays silently.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — versioning golden sets like code
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Walk through the git workflow for a golden-set repo: PRs, review, hash, tag, harness pin. Show what happens when a batch of 20 new rows is added — from the SME's local edit through the merged tag to the downstream metric join.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repo shape.&lt;/strong&gt; &lt;code&gt;golden/support_v3.jsonl&lt;/code&gt;, &lt;code&gt;golden/MANIFEST.json&lt;/code&gt; (with hash + row count), &lt;code&gt;golden/CHANGELOG.md&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PR flow.&lt;/strong&gt; Branch → edit → local check → PR → review → merge → tag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hash discipline.&lt;/strong&gt; Content hash goes in MANIFEST + git tag + downstream metric rows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Walk through the PR flow for adding 20 new billing-topic rows to a 280-row golden set, ending with the new hash and tag, and show how the harness pins to the tag.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Actor&lt;/th&gt;
&lt;th&gt;Artefact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. Sample&lt;/td&gt;
&lt;td&gt;SME&lt;/td&gt;
&lt;td&gt;20 real support tickets from the last week&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. Label&lt;/td&gt;
&lt;td&gt;SME&lt;/td&gt;
&lt;td&gt;expected_output for each&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. Peer review&lt;/td&gt;
&lt;td&gt;Reviewer&lt;/td&gt;
&lt;td&gt;approval or edits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4. Local check&lt;/td&gt;
&lt;td&gt;SME&lt;/td&gt;
&lt;td&gt;check_balance.py + hash regen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5. PR&lt;/td&gt;
&lt;td&gt;SME&lt;/td&gt;
&lt;td&gt;branch feature/golden-billing-batch-42&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6. CI&lt;/td&gt;
&lt;td&gt;GitHub Actions&lt;/td&gt;
&lt;td&gt;balance check + row-count diff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7. Merge&lt;/td&gt;
&lt;td&gt;Maintainer&lt;/td&gt;
&lt;td&gt;squash to main&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8. Tag&lt;/td&gt;
&lt;td&gt;Maintainer&lt;/td&gt;
&lt;td&gt;golden-v3.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9. Harness pin&lt;/td&gt;
&lt;td&gt;DE&lt;/td&gt;
&lt;td&gt;eval-config.yaml → version: golden-v3.3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1-2. Sample + label (SME's local flow)&lt;/span&gt;
git checkout &lt;span class="nt"&gt;-b&lt;/span&gt; feature/golden-billing-batch-42
python harness/sample_from_prod.py &lt;span class="nt"&gt;--topic&lt;/span&gt; billing &lt;span class="nt"&gt;--n&lt;/span&gt; 20 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; tmp/samples.jsonl
&lt;span class="c"&gt;# SME edits tmp/samples.jsonl to write expected_output + metadata for each row&lt;/span&gt;

&lt;span class="c"&gt;# 3. Peer review — SME opens PR, reviewer proposes edits, SME applies&lt;/span&gt;

&lt;span class="c"&gt;# 4. Local check&lt;/span&gt;
&lt;span class="nb"&gt;cat &lt;/span&gt;golden/support_v3.jsonl tmp/samples.jsonl &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; golden/support_v3.jsonl.new
&lt;span class="nb"&gt;mv &lt;/span&gt;golden/support_v3.jsonl.new golden/support_v3.jsonl
python harness/check_balance.py golden/support_v3.jsonl   &lt;span class="c"&gt;# WARN or BLOCK check&lt;/span&gt;
python harness/hash_manifest.py golden/support_v3.jsonl   &lt;span class="c"&gt;# writes MANIFEST.json&lt;/span&gt;
git diff golden/MANIFEST.json                              &lt;span class="c"&gt;# confirms hash bump&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;golden/MANIFEST.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(after&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;merge)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"v3.3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sha"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"b83a11"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rows"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"class_balance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"billing"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;78&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"account"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;55&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"product"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"shipping"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"other"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;62&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"generated_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-04T09:12:33Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"generator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"harness/hash_manifest.py"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# eval-config.yaml — harness pins to a specific tag&lt;/span&gt;
&lt;span class="na"&gt;golden_set&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;git@github.com:acme/golden-sets.git&lt;/span&gt;
  &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;golden/support_v3.jsonl&lt;/span&gt;
  &lt;span class="na"&gt;tag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;golden-v3.3&lt;/span&gt;
  &lt;span class="na"&gt;expected_sha&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;b83a11&lt;/span&gt;    &lt;span class="c1"&gt;# harness aborts if mismatch&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Every metric row joins by (run_id, golden_set_sha)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;golden_set_sha&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;row_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prediction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;judge_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;difficulty&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;eval_metric_rows&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt;   &lt;span class="n"&gt;golden_set_rows&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt;   &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;golden_set_sha&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sha&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt;  &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;row_id&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;row_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The SME branches off main, samples 20 real production tickets via a helper, and labels each with an expected answer + metadata. The samples are typed into JSONL in a local scratch file, then concatenated onto the existing golden set.&lt;/li&gt;
&lt;li&gt;Peer review happens in the PR. The reviewer proposes edits to expected answers (typos, style, missing empathy), the SME applies them, and the PR is re-pushed. Only reviewer-approved rows land in main.&lt;/li&gt;
&lt;li&gt;The local check runs &lt;code&gt;check_balance.py&lt;/code&gt; — in this case &lt;code&gt;billing&lt;/code&gt; moves from 58/280 (21 percent) to 78/300 (26 percent), triggering a WARN but not a BLOCK. The SME chooses to keep the imbalance for now with a plan to add shipping rows next batch.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hash_manifest.py&lt;/code&gt; computes the new SHA (&lt;code&gt;b83a11&lt;/code&gt;) and writes MANIFEST.json. The diff on MANIFEST.json is what the reviewer signs off on — a mechanical audit trail of "row count went from 280 to 300, hash went from e1f2c9 to b83a11."&lt;/li&gt;
&lt;li&gt;The harness pins to &lt;code&gt;tag: golden-v3.3&lt;/code&gt; + &lt;code&gt;expected_sha: b83a11&lt;/code&gt;. On load, it verifies both — if the tag has been re-pointed or the file has been edited without a hash bump, the harness aborts and the run does not corrupt the metric table. Every metric row also carries the SHA so downstream analysis can never confuse pre- and post-v3.3 scores.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Artefact&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rows&lt;/td&gt;
&lt;td&gt;280&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SHA&lt;/td&gt;
&lt;td&gt;e1f2c9&lt;/td&gt;
&lt;td&gt;b83a11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tag&lt;/td&gt;
&lt;td&gt;golden-v3.2&lt;/td&gt;
&lt;td&gt;golden-v3.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CHANGELOG.md entry&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;"v3.3: +20 billing rows from prod week 27"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Harness pin&lt;/td&gt;
&lt;td&gt;v3.2&lt;/td&gt;
&lt;td&gt;v3.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Downstream metric join key&lt;/td&gt;
&lt;td&gt;(run_id, e1f2c9)&lt;/td&gt;
&lt;td&gt;(run_id, b83a11)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Golden sets deserve the same git rigour as production migrations: branch, PR, review, tag, hash-pin. Skipping any of these is how a Monday score becomes incomparable to a Friday score without anyone noticing until an interview.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on golden-set design
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "You inherit a team where the golden set is a shared Google Sheet with 40 rows that everyone edits. Walk me through the migration to a versioned, class-balanced golden set of 300 rows in 6 weeks, without stalling the team's evaluation cadence."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a six-week golden-set migration plan with a hybrid harness
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# migration-plan.yaml — 6 weeks from Google Sheet to versioned golden set&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1.0&lt;/span&gt;

&lt;span class="na"&gt;week_1&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;goal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;freeze current state + baseline metric&lt;/span&gt;
  &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;snapshot the sheet as-is into git as golden/support_v0.jsonl&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;compute content_hash + tag as golden-v0-baseline&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;run the existing harness against v0-baseline for a 4-week baseline&lt;/span&gt;
  &lt;span class="na"&gt;deliverable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;golden-v0-baseline in git; baseline metric on dashboard&lt;/span&gt;

&lt;span class="na"&gt;week_2&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;goal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;schema migration&lt;/span&gt;
  &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;migrate 40 rows to (input, expected_output, metadata) schema&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;add topic + difficulty + language metadata (SME retroactively fills)&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;stand up check_balance.py + GitHub Actions&lt;/span&gt;
  &lt;span class="na"&gt;deliverable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;golden-v1.0 in git (40 rows, schema-conformant)&lt;/span&gt;

&lt;span class="na"&gt;week_3&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;goal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;first curation batch — sample 60 fresh production rows&lt;/span&gt;
  &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;build sample_from_prod.py (random sample from live conversations)&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;SME labels 60 rows across 5 topic classes (12 each)&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;peer review + merge&lt;/span&gt;
  &lt;span class="na"&gt;deliverable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;golden-v2.0 (100 rows, mostly balanced)&lt;/span&gt;

&lt;span class="na"&gt;week_4&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;goal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;second curation batch — 100 more rows&lt;/span&gt;
  &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;sample another 100 rows, focusing on under-represented classes&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;LLM-augmented labels + SME review (cheaper labelling)&lt;/span&gt;
  &lt;span class="na"&gt;deliverable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;golden-v2.1 (200 rows, balance within ±5%)&lt;/span&gt;

&lt;span class="na"&gt;week_5&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;goal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hybrid harness — score against both v0-baseline AND v2.1&lt;/span&gt;
  &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;harness runs against both sets on every PR&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dashboard shows both metrics side-by-side&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;team gets used to the new numbers before cutover&lt;/span&gt;
  &lt;span class="na"&gt;deliverable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hybrid dashboard live&lt;/span&gt;

&lt;span class="na"&gt;week_6&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;goal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cutover&lt;/span&gt;
  &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;final +100 rows to reach &lt;/span&gt;&lt;span class="m"&gt;300&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;retire v0-baseline; harness gates on v3.0 only&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;CHANGELOG + team announcement + runbook&lt;/span&gt;
  &lt;span class="na"&gt;deliverable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;golden-v3.0 (300 rows) as sole gating set&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Week&lt;/th&gt;
&lt;th&gt;Activity&lt;/th&gt;
&lt;th&gt;Rows&lt;/th&gt;
&lt;th&gt;Metric availability&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Freeze sheet, tag v0&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;Old-sheet baseline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Schema migration&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;v1.0 available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;+60 rows&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;v2.0 available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;+100 rows&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;v2.1 available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Hybrid harness&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;Both v0 and v2.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Cutover to v3.0&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;td&gt;v3.0 is the gate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After 6 weeks, the team has a 300-row, class-balanced, git-versioned golden set with automatic balance checks on every PR, a downstream metric table joined by SHA, and a runbook for the next migration when the schema changes again. The old sheet is archived; no one edits it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Week 1&lt;/th&gt;
&lt;th&gt;Week 6&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Golden-set rows&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;Google Sheet&lt;/td&gt;
&lt;td&gt;Git JSONL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Class balance&lt;/td&gt;
&lt;td&gt;unmeasured&lt;/td&gt;
&lt;td&gt;±5% per class&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Version control&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;git tag + SHA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PR review&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;reviewer required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Downstream metric join&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;(run_id, SHA)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Detectable regression&lt;/td&gt;
&lt;td&gt;~2.5%&lt;/td&gt;
&lt;td&gt;~1.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Freeze first, migrate second&lt;/strong&gt;&lt;/strong&gt; — the week-1 baseline lets the team detect regressions during the migration itself; without it, a schema change might silently mask a real quality drop. This is the same "capture golden state, then refactor" pattern you use for legacy pipeline migrations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Schema migration before curation&lt;/strong&gt;&lt;/strong&gt; — moving to the &lt;code&gt;(input, expected_output, metadata)&lt;/code&gt; shape unblocks every future capability (slice by topic, class-balance check, forbidden-pattern guard). Trying to curate before the schema is stable produces rows you have to re-edit later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Hybrid harness in week 5&lt;/strong&gt;&lt;/strong&gt; — running both metrics side-by-side is the safety net for cutover week. The team gets to compare "old sheet said 0.87, new golden set says 0.82" and understand the delta before it becomes a gate. Big-bang cutovers are how you break trust in the metric.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;LLM-augmented labelling in week 4&lt;/strong&gt;&lt;/strong&gt; — SMEs write 100 rows in weeks 2–3, then LLM-augmented labelling with SME review handles the next 100. The 5–10× speedup on the second batch is what makes the 6-week timeline realistic on a mid-size team.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — 6 weeks of one SME's part-time attention plus roughly 40 senior-engineer hours for schema, harness, and CI plumbing. The alternative (running on a 40-row sheet forever) has an unbounded downside; the migration cost is paid once and the golden set compounds in value every week thereafter.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;ETL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — etl&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;ETL problems on versioned dataset curation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;Data analysis&lt;/span&gt;
&lt;span&gt;Topic — data-analysis&lt;/span&gt;
&lt;strong&gt;Data-analysis problems on class balance and slicing&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/data-analysis" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  3. Cosine similarity + embedding-based scoring
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Cosine similarity is the cheap, fast, structural sanity check — and a lousy standalone truth metric
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;&lt;code&gt;cosine similarity&lt;/code&gt; on embeddings of &lt;code&gt;prediction&lt;/code&gt; and &lt;code&gt;expected_output&lt;/code&gt; is the fastest, cheapest, most-deterministic LLM-eval metric — perfect as a sanity check and a hallucination detector, terrible as a standalone truth metric because it cannot distinguish paraphrase from contradiction&lt;/strong&gt;. Every &lt;code&gt;llm eval pipeline&lt;/code&gt; should compute cosine as the &lt;em&gt;first&lt;/em&gt; metric on every row, and every serious &lt;code&gt;rag evaluation&lt;/code&gt; stack should also compute an LLM-judge score on top for the semantic axis cosine misses.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6254t3yjpibsk5qjyrkk.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6254t3yjpibsk5qjyrkk.jpeg" alt="Iconographic cosine similarity diagram — two vector arrows in a 2D plane forming a small angle, a cosine-gauge on the right, and a threshold-band chip." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four axes of cosine-based scoring.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Embedding model.&lt;/strong&gt; The cosine score is a function of the embedding — &lt;code&gt;text-embedding-3-large&lt;/code&gt; produces different distributions than &lt;code&gt;text-embedding-3-small&lt;/code&gt; or &lt;code&gt;bge-large&lt;/code&gt;. Pin the model, version-stamp the score column, and never change models mid-experiment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Threshold.&lt;/strong&gt; A cosine of 0.85 might be "correct" on one embedding model and "irrelevant" on another. The threshold must be &lt;em&gt;calibrated per embedding model per task&lt;/em&gt; against the golden set, not carried across from a tutorial.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failure modes.&lt;/strong&gt; Cosine treats paraphrase and contradiction as similar. &lt;code&gt;"Yes, you can cancel anytime"&lt;/code&gt; and &lt;code&gt;"No, you cannot cancel"&lt;/code&gt; embed close to each other; cosine sails through, the judge (and the customer) does not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aggregation.&lt;/strong&gt; Per-row cosine is a float in &lt;a href="https://dev.toin%20practice%20[0,%201]%20for%20normalized%20embeddings%20on%20well-formed%20text"&gt;-1, 1&lt;/a&gt;. Aggregate as mean over rows for the summary; also compute per-class means for the dashboard slice.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How embeddings become a metric.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Step 1.&lt;/strong&gt; Embed the &lt;code&gt;expected_output&lt;/code&gt; string with the chosen embedding model; store as a &lt;code&gt;float32[dim]&lt;/code&gt; vector (dim=3072 for &lt;code&gt;text-embedding-3-large&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 2.&lt;/strong&gt; Embed the model's &lt;code&gt;prediction&lt;/code&gt; with the &lt;em&gt;same&lt;/em&gt; embedding model. The vectors live in the same space; cosine is defined.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 3.&lt;/strong&gt; Compute cosine similarity: &lt;code&gt;dot(a, b) / (norm(a) × norm(b))&lt;/code&gt;. For normalized embeddings this is a dot product.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 4.&lt;/strong&gt; Compare to the threshold. Above threshold = "correct" (cheap-check pass); below = "flagged" (escalate to judge for the semantic axis).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Threshold calibration — the numeric discipline.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gather.&lt;/strong&gt; 200 golden-set rows plus 200 known-wrong predictions (paraphrased edits, contradiction injections).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Score.&lt;/strong&gt; Compute cosine for all 400 pairs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choose.&lt;/strong&gt; Pick the threshold that maximizes F1 on the "correct vs wrong" classification. Typical values land at 0.75–0.90 for &lt;code&gt;text-embedding-3-large&lt;/code&gt; on medium-length answers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version-stamp.&lt;/strong&gt; The threshold is a function of the embedding model + task; store it in the eval config with the model version. Bumping the embedding model always requires re-calibration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Failure modes cosine cannot catch alone.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Contradiction.&lt;/strong&gt; &lt;code&gt;"Yes"&lt;/code&gt; and &lt;code&gt;"No"&lt;/code&gt; embed as nearby short strings. Cosine misses the polarity flip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paraphrase.&lt;/strong&gt; &lt;code&gt;"Update via Settings → Billing"&lt;/code&gt; and &lt;code&gt;"Change in the Billing settings menu"&lt;/code&gt; are semantically identical but embed differently on some models — cosine can &lt;em&gt;under&lt;/em&gt;-count paraphrase correctness.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Style mismatch.&lt;/strong&gt; A perfectly correct answer in a formal tone will score high cosine against an empathetic expected answer even though the tone-brand is off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long-form dilution.&lt;/strong&gt; A 500-token answer embeds "average"-ish; a small hallucinated URL inside it barely moves the vector. Cosine is blind to needle-in-haystack failure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When cosine actually shines.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Structural hallucination detection.&lt;/strong&gt; Predictions with wrong entities, wrong URLs, wrong dates score dramatically lower than paraphrased-but-correct alternatives. Cosine is a great first-pass anomaly detector.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regression detection.&lt;/strong&gt; A prompt change that drops mean cosine by 0.10 across 300 rows is &lt;em&gt;always&lt;/em&gt; a regression worth investigating, even if individual rows look fine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speed.&lt;/strong&gt; Embeddings cost fractions of a cent; cosine is a matrix multiply. Perfect for every-PR gating where an LLM-judge would be too slow or expensive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Determinism.&lt;/strong&gt; Cosine is deterministic for a fixed embedding model. Unlike judges, the score is reproducible bit-for-bit — great for baseline math.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common cosine pitfalls in the wild.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Un-normalized embeddings.&lt;/strong&gt; Forgetting to normalize the vectors turns cosine into "dot product times garbage." Always L2-normalize before comparing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model version drift.&lt;/strong&gt; OpenAI silently ships new embedding checkpoints under the same public name; pin to a specific model version and log it with every score row.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mixed languages.&lt;/strong&gt; Cross-lingual cosine is much noisier than same-language. Slice the metric by language before drawing conclusions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Threshold from a Colab notebook.&lt;/strong&gt; The threshold everyone quotes came from someone's exploration script and was never re-calibrated. Re-run calibration on every embedding-model bump.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What senior interviewers listen for.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do you name &lt;strong&gt;cosine as a sanity check, not a truth metric&lt;/strong&gt;, and pair it with a judge? — required answer.&lt;/li&gt;
&lt;li&gt;Do you volunteer &lt;strong&gt;paraphrase vs contradiction&lt;/strong&gt; as the canonical failure modes? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you name &lt;strong&gt;threshold calibration&lt;/strong&gt; as a per-embedding-model discipline rather than a constant? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you distinguish &lt;strong&gt;normalized vs unnormalized&lt;/strong&gt; and defend the L2 normalization step? — required answer.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — cosine scoring a 300-row golden set with text-embedding-3
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Build the cosine scorer against a 300-row golden set using OpenAI's &lt;code&gt;text-embedding-3-large&lt;/code&gt;. Batch the embedding calls for cost, L2-normalize the vectors, compute per-row cosine, aggregate by topic class, and produce the summary table with the calibrated 0.85 threshold.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Embedding model.&lt;/strong&gt; &lt;code&gt;text-embedding-3-large&lt;/code&gt; (dim=3072).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch size.&lt;/strong&gt; 100 rows per API call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Threshold.&lt;/strong&gt; 0.85 (previously calibrated).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aggregation.&lt;/strong&gt; Per-class mean + overall mean + fraction ≥ threshold.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Implement the cosine scorer end-to-end. Show the batch embedding, normalization, cosine computation, and per-class aggregation. Report the summary table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Golden set&lt;/td&gt;
&lt;td&gt;300 rows, 5 topic classes × 60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Embedding model&lt;/td&gt;
&lt;td&gt;text-embedding-3-large (dim=3072)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch size&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Threshold&lt;/td&gt;
&lt;td&gt;0.85&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expected cost&lt;/td&gt;
&lt;td&gt;300 × 2 embeddings × $0.00013 / 1K tokens × ~200 tokens = ~$0.016&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# cosine_scorer.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Iterable&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;EMBED_MODEL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-embedding-3-large&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;BATCH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;embed_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;texts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return an (n, dim) float32 array of L2-normalized embeddings.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;EMBED_MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;texts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;vecs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asarray&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;float32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;norms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linalg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vecs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keepdims&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;vecs&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;norms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1e-9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;batched&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Iterable&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cosine_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;predictions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    rows        : golden set (must include row_id, expected_output, metadata.topic)
    predictions : row_id -&amp;gt; model prediction string
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expected_output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;predicted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;predictions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;row_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;exp_vecs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;vstack&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nf"&gt;embed_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;batched&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BATCH&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
    &lt;span class="n"&gt;pred_vecs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;vstack&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nf"&gt;embed_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;batched&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;predicted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BATCH&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;

    &lt;span class="c1"&gt;# Dot product of L2-normalized vectors == cosine similarity
&lt;/span&gt;    &lt;span class="n"&gt;cos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exp_vecs&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;pred_vecs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;row_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;row_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;topic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metadata&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;topic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cosine&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pass_0.85&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cos&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;summarize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;by_topic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;topic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;row_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;mean_cosine&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cosine&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mean&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;pass_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pass_0.85&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mean&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;reset_index&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;overall&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;([{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;topic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OVERALL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mean_cosine&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cosine&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pass_rate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pass_0.85&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;by_topic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;overall&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;ignore_index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;golden/support_v3.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
    &lt;span class="n"&gt;predictions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;runs/run_2026-07-04.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cosine_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;predictions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;summarize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;embed_batch&lt;/code&gt; sends 100 strings at a time to OpenAI's embeddings endpoint, receives 100 3072-dim vectors, stacks them into a NumPy array, and L2-normalizes so cosine reduces to a plain dot product.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cosine_score&lt;/code&gt; embeds the 300 expected outputs and the 300 predictions in parallel batches (3 calls each), then computes cosine as an element-wise multiply-and-sum on the L2-normalized vectors. Total wall-clock is dominated by the embedding calls (~2 seconds each) — roughly 6 seconds for the full run.&lt;/li&gt;
&lt;li&gt;Every row emits a &lt;code&gt;(row_id, topic, cosine, pass_0.85)&lt;/code&gt; record. The &lt;code&gt;pass_0.85&lt;/code&gt; boolean is the threshold check; downstream aggregation treats it as a per-class pass rate.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;summarize&lt;/code&gt; groups by topic to expose per-class means and pass rates, then appends an overall row. This is the table that goes into the PR comment — one row per topic plus overall.&lt;/li&gt;
&lt;li&gt;Cost math: 300 × 2 embeddings × ~200 tokens/row = 120,000 tokens; at $0.00013 / 1K tokens for &lt;code&gt;text-embedding-3-large&lt;/code&gt;, the scorer costs about $0.016 per full run. Effectively free at PR gate frequency.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;topic&lt;/th&gt;
&lt;th&gt;n&lt;/th&gt;
&lt;th&gt;mean_cosine&lt;/th&gt;
&lt;th&gt;pass_rate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;account&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;0.9215&lt;/td&gt;
&lt;td&gt;0.9500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;billing&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;0.8842&lt;/td&gt;
&lt;td&gt;0.8500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;other&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;0.9051&lt;/td&gt;
&lt;td&gt;0.9167&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;product&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;0.9127&lt;/td&gt;
&lt;td&gt;0.9333&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;shipping&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;0.8776&lt;/td&gt;
&lt;td&gt;0.8000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OVERALL&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;td&gt;0.9002&lt;/td&gt;
&lt;td&gt;0.8900&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Cosine scoring is a &lt;code&gt;pandas.groupby&lt;/code&gt; on top of a NumPy dot product. Keep it deterministic, cheap, and always paired with a judge for the semantic axis it cannot see.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — threshold calibration with a contradiction-injected set
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Cosine thresholds are meaningless without calibration. Build the calibrator: take 200 golden-set pairs (correct) plus 200 contradiction-injected pairs (wrong), compute cosine on all 400, sweep the threshold to maximize F1, and produce the calibration curve.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Positive class.&lt;/strong&gt; Correct predictions (from the golden set itself, expected as prediction).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Negative class.&lt;/strong&gt; Contradiction-injected predictions (LLM-generated wrong-polarity answers reviewed by an SME).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sweep.&lt;/strong&gt; Thresholds from 0.5 to 0.99 in 0.01 steps; report precision, recall, F1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choose.&lt;/strong&gt; Threshold that maximizes F1, unless business context prefers precision (fewer false-positive passes) or recall (fewer false-negative fails).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Build the calibrator, sweep the threshold, and pick the operating point for a chatbot where a &lt;em&gt;missed&lt;/em&gt; wrong answer is 3× more costly than a &lt;em&gt;false-flagged&lt;/em&gt; correct answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Correct pairs&lt;/td&gt;
&lt;td&gt;200 (from golden set)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contradiction pairs&lt;/td&gt;
&lt;td&gt;200 (LLM-generated, SME-reviewed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Embedding model&lt;/td&gt;
&lt;td&gt;text-embedding-3-large&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sweep&lt;/td&gt;
&lt;td&gt;0.50 to 0.99 in 0.01 steps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost ratio (miss : false-flag)&lt;/td&gt;
&lt;td&gt;3 : 1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# calibrate_threshold.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;cosine_scorer&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;embed_batch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BATCH&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cosine_pairs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;va&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;vstack&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nf"&gt;embed_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;BATCH&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;BATCH&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
    &lt;span class="n"&gt;vb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;vstack&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nf"&gt;embed_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;BATCH&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;BATCH&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;va&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;vb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sweep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cos_pos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cos_neg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cost_miss&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cost_ff&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;tp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cos_pos&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cos_pos&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;  &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;fp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cos_neg&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;tn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cos_neg&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;  &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;precision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tp&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tp&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;fp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tp&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;fp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
        &lt;span class="n"&gt;recall&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tp&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tp&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tp&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
        &lt;span class="n"&gt;f1&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;precision&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;recall&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;precision&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;recall&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;precision&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;recall&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
        &lt;span class="c1"&gt;# cost-weighted score: penalize false-flags (fn on positives) and missed-wrongs (fp on negatives)
&lt;/span&gt;        &lt;span class="n"&gt;cost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cost_ff&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;cost_miss&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;fp&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;threshold&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tp&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fp&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tn&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;precision&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;precision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;recall&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recall&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;f1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;pos_pairs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;calibration/correct_pairs.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;    &lt;span class="c1"&gt;# 200 pairs
&lt;/span&gt;    &lt;span class="n"&gt;neg_pairs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;calibration/contradiction_pairs.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;  &lt;span class="c1"&gt;# 200 pairs
&lt;/span&gt;
    &lt;span class="n"&gt;cos_pos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cosine_pairs&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pos_pairs&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prediction&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pos_pairs&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;cos_neg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cosine_pairs&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;neg_pairs&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prediction&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;neg_pairs&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sweep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cos_pos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cos_neg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cost_miss&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cost_ff&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;df_best_f1&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;f1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;idxmax&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
    &lt;span class="n"&gt;df_best_cost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;idxmin&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Best F1 threshold:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;df_best_f1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_dict&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Best cost-weighted threshold:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;df_best_cost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_dict&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Positive pairs are 200 rows where &lt;code&gt;prediction == expected_output&lt;/code&gt; (perfect answers). Negative pairs are 200 rows where a strong LLM was prompted to &lt;em&gt;contradict&lt;/em&gt; the expected answer, then an SME reviewed to ensure each is a genuine wrong-polarity flip. Both are embedded and cosine'd.&lt;/li&gt;
&lt;li&gt;The sweep computes confusion-matrix entries at each threshold: true positives (correct passes), false negatives (correct-but-flagged), false positives (wrong-but-passed), true negatives (wrong-and-flagged). Precision, recall, and F1 fall out of the confusion matrix.&lt;/li&gt;
&lt;li&gt;The cost function encodes the business preference: a missed wrong answer (FP on negatives) is 3× the pain of a false-flagged correct answer (FN on positives). The cost-optimal threshold is &lt;em&gt;higher&lt;/em&gt; than the F1-optimal threshold because higher thresholds catch more wrongs at the cost of flagging more corrects.&lt;/li&gt;
&lt;li&gt;Empirically on &lt;code&gt;text-embedding-3-large&lt;/code&gt;: F1 peaks around 0.82 (threshold 0.86); cost-optimal at 3:1 miss weight lands around 0.89. Both numbers are model-specific and answer-length-specific; they should be re-derived on your data.&lt;/li&gt;
&lt;li&gt;The chosen operating point (0.89) goes into &lt;code&gt;eval-config.yaml&lt;/code&gt; as &lt;code&gt;cosine.threshold: 0.89&lt;/code&gt; with a comment linking to the calibration report and the calibration set's git hash. Any future embedding-model bump requires a fresh calibration.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;threshold&lt;/th&gt;
&lt;th&gt;tp&lt;/th&gt;
&lt;th&gt;fn&lt;/th&gt;
&lt;th&gt;fp&lt;/th&gt;
&lt;th&gt;tn&lt;/th&gt;
&lt;th&gt;precision&lt;/th&gt;
&lt;th&gt;recall&lt;/th&gt;
&lt;th&gt;f1&lt;/th&gt;
&lt;th&gt;cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0.80&lt;/td&gt;
&lt;td&gt;195&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;158&lt;/td&gt;
&lt;td&gt;0.823&lt;/td&gt;
&lt;td&gt;0.975&lt;/td&gt;
&lt;td&gt;0.893&lt;/td&gt;
&lt;td&gt;131&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.85&lt;/td&gt;
&lt;td&gt;190&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;178&lt;/td&gt;
&lt;td&gt;0.896&lt;/td&gt;
&lt;td&gt;0.950&lt;/td&gt;
&lt;td&gt;0.922&lt;/td&gt;
&lt;td&gt;76&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.86&lt;/td&gt;
&lt;td&gt;187&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;185&lt;/td&gt;
&lt;td&gt;0.926&lt;/td&gt;
&lt;td&gt;0.935&lt;/td&gt;
&lt;td&gt;0.930&lt;/td&gt;
&lt;td&gt;58&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.89&lt;/td&gt;
&lt;td&gt;175&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;192&lt;/td&gt;
&lt;td&gt;0.956&lt;/td&gt;
&lt;td&gt;0.875&lt;/td&gt;
&lt;td&gt;0.914&lt;/td&gt;
&lt;td&gt;49&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.90&lt;/td&gt;
&lt;td&gt;168&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;195&lt;/td&gt;
&lt;td&gt;0.971&lt;/td&gt;
&lt;td&gt;0.840&lt;/td&gt;
&lt;td&gt;0.901&lt;/td&gt;
&lt;td&gt;47&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.92&lt;/td&gt;
&lt;td&gt;148&lt;/td&gt;
&lt;td&gt;52&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;198&lt;/td&gt;
&lt;td&gt;0.987&lt;/td&gt;
&lt;td&gt;0.740&lt;/td&gt;
&lt;td&gt;0.846&lt;/td&gt;
&lt;td&gt;58&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Calibrate the cosine threshold against a labeled positive+negative set, sweep with a cost function that reflects your business's miss/false-flag ratio, and pin the chosen threshold in config with a link back to the calibration report. Never quote a threshold "from the paper" or "from a blog post" — it will not match your data.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the paraphrase vs contradiction blind spot
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Demonstrate the canonical cosine failure mode with a synthetic 20-row set: 10 paraphrases (semantically identical, superficially different) and 10 contradictions (semantically opposite, superficially similar). Show cosine scores for each and prove why an LLM-judge is required on top.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Paraphrases.&lt;/strong&gt; Same meaning, different words. Cosine will &lt;em&gt;under&lt;/em&gt;-count these as false negatives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contradictions.&lt;/strong&gt; Opposite meaning, similar surface form. Cosine will &lt;em&gt;over&lt;/em&gt;-count these as false positives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Judge job.&lt;/strong&gt; Distinguish paraphrase from contradiction where cosine cannot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Build the demonstrator: score 10 paraphrase and 10 contradiction pairs with cosine, then with an LLM-as-judge, and compare their agreement + disagreement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pair type&lt;/th&gt;
&lt;th&gt;Example expected&lt;/th&gt;
&lt;th&gt;Example prediction&lt;/th&gt;
&lt;th&gt;Ground truth&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Paraphrase&lt;/td&gt;
&lt;td&gt;Yes, you can cancel anytime.&lt;/td&gt;
&lt;td&gt;You may cancel at any time.&lt;/td&gt;
&lt;td&gt;correct&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Paraphrase&lt;/td&gt;
&lt;td&gt;The refund arrives in 3-5 business days.&lt;/td&gt;
&lt;td&gt;Refunds land within 3 to 5 working days.&lt;/td&gt;
&lt;td&gt;correct&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contradiction&lt;/td&gt;
&lt;td&gt;Yes, you can cancel anytime.&lt;/td&gt;
&lt;td&gt;No, cancellation is not permitted.&lt;/td&gt;
&lt;td&gt;wrong&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contradiction&lt;/td&gt;
&lt;td&gt;The refund arrives in 3-5 business days.&lt;/td&gt;
&lt;td&gt;Refunds are not available.&lt;/td&gt;
&lt;td&gt;wrong&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# cosine_vs_judge_demo.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;cosine_scorer&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;embed_batch&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;llm_judge&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;judge_row&lt;/span&gt;       &lt;span class="c1"&gt;# defined in Section 4
&lt;/span&gt;

&lt;span class="n"&gt;pairs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="c1"&gt;# Paraphrases (correct)
&lt;/span&gt;    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Yes, you can cancel anytime.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You may cancel at any time.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                          &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;correct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The refund arrives in 3-5 business days.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Refunds land within 3 to 5 working days.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;correct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Please email support@company.com for help.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;For assistance, contact support@company.com.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;correct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Your order will ship tomorrow.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The shipment is scheduled for tomorrow.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;              &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;correct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;We accept Visa and Mastercard.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Visa and Mastercard payments are accepted.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;correct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;This feature is available on Pro plans.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Pro plans include this feature.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                      &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;correct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ll need admin access to change settings.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Admin permissions are required to modify settings.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;correct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The trial lasts 14 days.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                      &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You get a 14-day trial period.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;correct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Data exports are in CSV format.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;               &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Exported data is provided as CSV files.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;              &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;correct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The API rate limit is 1000 requests/min.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You can make up to 1000 API calls per minute.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;correct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="c1"&gt;# Contradictions (wrong)
&lt;/span&gt;    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Yes, you can cancel anytime.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No, cancellation is not permitted.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wrong&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The refund arrives in 3-5 business days.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Refunds are not available.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wrong&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Please email support@company.com for help.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Do not email support; they don&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;t respond.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wrong&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Your order will ship tomorrow.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Your order has been cancelled.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wrong&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;We accept Visa and Mastercard.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;We do not accept credit cards.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wrong&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;This feature is available on Pro plans.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;This feature is not available on any plan.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wrong&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ll need admin access to change settings.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Anyone can change settings without any permission.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wrong&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The trial lasts 14 days.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                      &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;There is no trial period offered.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wrong&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Data exports are in CSV format.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;               &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Data cannot be exported.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                             &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wrong&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The API rate limit is 1000 requests/min.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;There is no API access available.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wrong&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;demo&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;exp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;embed_batch&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;pred&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;embed_batch&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;cos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exp&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;pred&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="nf"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;truth&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;judge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;judge_row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(hidden)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prediction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# returns 0 or 1
&lt;/span&gt;        &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;paraphrase&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;truth&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;correct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contradiction&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cosine&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cos_pass&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;judge&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;judge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;truth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;truth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="n"&gt;cos_acc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cos_pass&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;truth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;correct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cos_pass&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;truth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wrong&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))).&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;jud_acc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;judge&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;truth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;correct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;judge&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;truth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wrong&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;cosine  agreement with truth: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cos_acc&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;judge   agreement with truth: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;jud_acc&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nf"&gt;demo&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The 20 pairs are hand-designed: 10 paraphrases (semantically identical, different surface form) and 10 contradictions (opposite polarity, otherwise similar surface form). This is a stress test built to break a naïve cosine gate.&lt;/li&gt;
&lt;li&gt;Cosine scores paraphrases in the 0.75–0.85 range (below the calibrated 0.85 threshold — false negatives on the &lt;em&gt;correct&lt;/em&gt; class) and contradictions in the 0.80–0.90 range (some above the threshold — false positives on the &lt;em&gt;wrong&lt;/em&gt; class). The threshold cannot separate the two populations cleanly.&lt;/li&gt;
&lt;li&gt;The LLM-judge is called with the full &lt;code&gt;(expected, prediction)&lt;/code&gt; pair plus a rubric. For paraphrases it recognizes the semantic equivalence and returns 1 (pass); for contradictions it recognizes the polarity flip and returns 0 (fail). Judge accuracy on this stress set is typically 95–100 percent.&lt;/li&gt;
&lt;li&gt;The comparison table exposes the exact gap: cosine agreement with ground truth is around 55–65 percent; judge agreement is 95–100 percent. On real production data the gap narrows (real predictions are less adversarial), but the direction is always the same — judge on top of cosine.&lt;/li&gt;
&lt;li&gt;The lesson is not "throw cosine away." It is "cosine as first-pass filter, judge as truth arbiter." Cosine catches structural hallucinations cheaply; the judge catches semantic errors expensively. Use both, and know which failure mode each catches.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;type&lt;/th&gt;
&lt;th&gt;cosine&lt;/th&gt;
&lt;th&gt;cos_pass&lt;/th&gt;
&lt;th&gt;judge&lt;/th&gt;
&lt;th&gt;truth&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;paraphrase&lt;/td&gt;
&lt;td&gt;0.816&lt;/td&gt;
&lt;td&gt;False&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;correct&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;paraphrase&lt;/td&gt;
&lt;td&gt;0.892&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;correct&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;paraphrase&lt;/td&gt;
&lt;td&gt;0.751&lt;/td&gt;
&lt;td&gt;False&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;correct&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;contradiction&lt;/td&gt;
&lt;td&gt;0.828&lt;/td&gt;
&lt;td&gt;False&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;wrong&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;contradiction&lt;/td&gt;
&lt;td&gt;0.874&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;wrong&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;contradiction&lt;/td&gt;
&lt;td&gt;0.845&lt;/td&gt;
&lt;td&gt;False&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;wrong&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Cosine agreement with truth: ~60 percent. Judge agreement with truth: ~100 percent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Any metric that cannot distinguish "yes" from "no" on similar surface forms is a false-security metric. Cosine is a great cheap gate; the judge is the truth signal that keeps the metric honest.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on cosine-based scoring
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Walk me through your cosine similarity scorer for a RAG chatbot. How do you pick the embedding model, calibrate the threshold, handle multilingual traffic, and pair it with a judge without doubling your eval cost?"&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using pinned embedding + calibrated threshold + judge-on-miss escalation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# cosine_plus_judge.py — the reference cosine + judge orchestrator
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;cosine_scorer&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;embed_batch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EMBED_MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BATCH&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;llm_judge&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;judge_row&lt;/span&gt;

&lt;span class="c1"&gt;# Pinned in config; bump forces recalibration
&lt;/span&gt;&lt;span class="n"&gt;EMBEDDING_VERSION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-embedding-3-large@2024-11-01&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;THRESHOLD_BY_LANGUAGE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.86&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;es&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.83&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.83&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;default&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.82&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;score_row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected_vec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prediction_vec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected_vec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prediction_vec&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;threshold_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;language&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;THRESHOLD_BY_LANGUAGE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;language&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;THRESHOLD_BY_LANGUAGE&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;default&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;score_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;predictions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;exp_vecs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;vstack&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="nf"&gt;embed_batch&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expected_output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;BATCH&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;BATCH&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;pred_vecs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;vstack&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="nf"&gt;embed_batch&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;predictions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;row_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;BATCH&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;BATCH&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pv&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exp_vecs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pred_vecs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;lang&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metadata&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;language&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;score_row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;thr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;threshold_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;cos_pass&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;thr&lt;/span&gt;
        &lt;span class="c1"&gt;# Judge-on-miss: escalate only when cosine says "wrong"
&lt;/span&gt;        &lt;span class="n"&gt;judge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;judge_row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;input_text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expected_output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;prediction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;predictions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;row_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
            &lt;span class="n"&gt;rubric&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;correctness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;cos_pass&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;row_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;row_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;topic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metadata&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;topic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;language&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cosine&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cos_threshold&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="n"&gt;thr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cos_pass&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;cos_pass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;judge_score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;judge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;final_pass&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;cos_pass&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;judge&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;judge&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;embedding&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;EMBEDDING_VERSION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;golden/support_v3.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
    &lt;span class="n"&gt;predictions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;runs/run_2026-07-04.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;score_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;predictions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupby&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;topic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;language&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;row_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;mean_cosine&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cosine&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mean&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;cos_pass_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cos_pass&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mean&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;final_pass_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;final_pass&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mean&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_markdown&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pin embedding&lt;/td&gt;
&lt;td&gt;text-embedding-3-large@2024-11-01&lt;/td&gt;
&lt;td&gt;version stamp on every row&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Embed expected + prediction&lt;/td&gt;
&lt;td&gt;300 rows&lt;/td&gt;
&lt;td&gt;600 3072-dim vectors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compute cosine&lt;/td&gt;
&lt;td&gt;dot(ev, pv) after L2-norm&lt;/td&gt;
&lt;td&gt;300 floats in [0, 1]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Threshold by language&lt;/td&gt;
&lt;td&gt;en=0.86, es/fr=0.83&lt;/td&gt;
&lt;td&gt;300 bool pass flags&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Escalate on miss&lt;/td&gt;
&lt;td&gt;~40 rows below threshold&lt;/td&gt;
&lt;td&gt;40 judge calls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aggregate final pass&lt;/td&gt;
&lt;td&gt;cos_pass OR judge≥0.7&lt;/td&gt;
&lt;td&gt;300 final bool&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After the rollout, cosine handles ~87 percent of rows for free (below-threshold rows escalate to the judge); judge is called on only the ~13 percent that cosine flagged. Total judge cost drops by ~7× vs judging every row, while the final metric is judge-quality on the disputed subset.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Cost per full run&lt;/th&gt;
&lt;th&gt;Detects paraphrase&lt;/th&gt;
&lt;th&gt;Detects contradiction&lt;/th&gt;
&lt;th&gt;Detects structural&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cosine only&lt;/td&gt;
&lt;td&gt;$0.02&lt;/td&gt;
&lt;td&gt;partial&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Judge only&lt;/td&gt;
&lt;td&gt;$2.00&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;partial&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cosine + judge-on-miss&lt;/td&gt;
&lt;td&gt;$0.30&lt;/td&gt;
&lt;td&gt;yes (via judge)&lt;/td&gt;
&lt;td&gt;yes (via judge)&lt;/td&gt;
&lt;td&gt;yes (cosine)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Pinned embedding version&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;text-embedding-3-large@2024-11-01&lt;/code&gt; is written into every score row so the metric is comparable across time. Bumping the embedding model is a &lt;em&gt;first-class&lt;/em&gt; change that requires re-calibrating the threshold; the version stamp makes it impossible to forget.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Per-language thresholds&lt;/strong&gt;&lt;/strong&gt; — cross-lingual embeddings noisy differently per language pair; carrying a single "0.85" threshold across en/es/fr silently penalizes non-English rows. Per-language calibration is a one-hour exercise per language, paid once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Judge-on-miss escalation&lt;/strong&gt;&lt;/strong&gt; — cosine handles the majority of rows for essentially free; the judge is called only when cosine says "wrong." This preserves judge-quality decisions on the disputed subset while cutting judge cost by roughly 7× on a well-performing model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;final_pass logic&lt;/strong&gt;&lt;/strong&gt; — a row passes if cosine says yes OR (cosine said no AND judge says ≥0.7). The judge acts as an &lt;em&gt;appeal&lt;/em&gt; for cosine's false negatives on paraphrases; the OR-logic ensures paraphrases don't get punished, while contradictions still fail because the judge does not overturn cosine's &lt;em&gt;passes&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — per-run: ~$0.02 embeddings + ~$0.30 judge on the escalated 13 percent = ~$0.32. The all-judge alternative is ~$2.00 (6× more expensive) with only marginal quality gain on rows cosine already handled correctly. Judge-on-miss is the price-performance sweet spot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Data analysis&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — data-analysis&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Data-analysis problems on similarity and threshold calibration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/data-analysis" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;Optimization&lt;/span&gt;
&lt;span&gt;Topic — optimization&lt;/span&gt;
&lt;strong&gt;Optimization problems on scorer cost and coverage&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/optimization" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  4. LLM-as-judge — GPT-4-class rubric scorer
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The judge is a rubric + a strong model + a structured JSON schema — anything less is vibes
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;&lt;code&gt;llm as judge&lt;/code&gt; is the pattern where a strong model (GPT-4-class or better) scores the production model's prediction against the golden expected answer using an explicit rubric, returning a structured JSON with a numeric score plus a rationale — and its power comes entirely from the rubric quality, the model choice, and the output schema, not from any magic in "the LLM said so"&lt;/strong&gt;. A judge without a rubric is vibes; a rubric without a strong judge is a rubric being ignored; a judge with a rubric returning free-form prose is unusable as a metric. Engineer all three.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy9w8h0fzljpihyjx5ou9.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy9w8h0fzljpihyjx5ou9.jpeg" alt="Iconographic LLM-as-judge diagram — a rubric scroll on the left, a large judge robot glyph in the middle with a gavel, and a JSON output card on the right with score and rationale." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four axes of a serious judge.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rubric.&lt;/strong&gt; An explicit list of criteria with clear pass/fail language. Typical rubrics carry 2–4 criteria (correctness, completeness, style, safety); more than 4 dilutes the judge's attention; fewer than 2 collapses the metric to a single dimension.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model.&lt;/strong&gt; GPT-4o, Claude Opus, or equivalent. Judge quality tracks base-model reasoning quality; using the &lt;em&gt;same&lt;/em&gt; model as production for judgement risks correlated failure modes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output schema.&lt;/strong&gt; Structured JSON with typed fields (&lt;code&gt;score: float&lt;/code&gt;, &lt;code&gt;rationale: string&lt;/code&gt;, optional &lt;code&gt;breakdown&lt;/code&gt; per criterion). Function-calling or JSON-mode is mandatory — free-form text is unparseable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost + sampling.&lt;/strong&gt; Judge cost per row × golden-set size × runs per week is the finance-visible number. Sample intelligently (per-PR 20 percent sample + nightly full-set) to keep the bill sane.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The rubric — write it like an SLA.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Explicit criteria.&lt;/strong&gt; &lt;code&gt;correctness&lt;/code&gt; (is the factual content right?), &lt;code&gt;completeness&lt;/code&gt; (are the required facts included?), &lt;code&gt;style&lt;/code&gt; (is the tone appropriate?). Each criterion gets its own explicit language.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scored on a scale.&lt;/strong&gt; 0.0–1.0 or 1–5. Continuous scales let you aggregate as means; discrete scales are easier for judges to calibrate but coarser downstream.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grounded in the expected answer.&lt;/strong&gt; The judge always has access to the expected answer as ground truth; without it, the judge is just re-answering the question rather than scoring.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failure modes named.&lt;/strong&gt; The rubric explicitly names the failure modes to look for: &lt;code&gt;"a wrong URL", "a wrong dollar amount", "an empathy mismatch"&lt;/code&gt;. This forces the judge to check specifics rather than hand-wave.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The output schema — never take free-form text.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use JSON mode / function calling.&lt;/strong&gt; OpenAI's &lt;code&gt;response_format={"type": "json_object"}&lt;/code&gt;, Anthropic's tool use, or Gemini's structured output. The judge returns typed fields; parsing is trivial.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type every field.&lt;/strong&gt; &lt;code&gt;score: float in [0, 1]&lt;/code&gt;, &lt;code&gt;rationale: string&lt;/code&gt;, &lt;code&gt;breakdown: {correctness: float, completeness: float, style: float}&lt;/code&gt;. The schema is verified with pydantic on receive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bounded output length.&lt;/strong&gt; Cap rationale at 500 tokens. Longer rationales cost more without improving the score signal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotence.&lt;/strong&gt; Set &lt;code&gt;temperature=0&lt;/code&gt; for reproducible judgements. Non-zero temperature turns the judge into a random noise generator.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The judge model — do not use the production model.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Same-family risk.&lt;/strong&gt; Using GPT-4o-mini as judge for a GPT-4o-mini production model risks correlated failure modes — the judge fails to notice the same errors the production model made.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recommendation.&lt;/strong&gt; Judge with a &lt;em&gt;stronger&lt;/em&gt; or &lt;em&gt;different-family&lt;/em&gt; model. GPT-4o judges GPT-4o-mini production; Claude Opus judges GPT-4o production; Gemini Ultra judges Claude Sonnet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost trade-off.&lt;/strong&gt; A stronger judge costs more per row. Balance with sample-on-PR + full-nightly (cheap judge on the sample, stronger judge on nightly calibration).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Calibrate cross-judge agreement.&lt;/strong&gt; Run 100 golden rows through two judges (GPT-4o and Claude Opus). Compute agreement (Cohen's κ or Pearson r on scores). Above 0.85 means either judge is fine; below 0.7 means the rubric needs tightening.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Failure modes of the judge itself.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Judge bias.&lt;/strong&gt; Judges have systematic preferences (length, formality, structure). A verbose but wrong answer may score higher than a concise correct one. Counter with explicit anti-bias language in the rubric.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt sensitivity.&lt;/strong&gt; Small phrasing changes in the rubric shift scores. Version-hash the rubric and treat rubric edits like migrations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Position bias.&lt;/strong&gt; Some judges systematically favor the &lt;em&gt;first&lt;/em&gt; option in a comparison; randomize the order in pairwise comparison setups.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rubric drift.&lt;/strong&gt; Over time the SME's mental rubric drifts from the written rubric. Refresh the rubric every quarter and re-calibrate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What senior interviewers listen for.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do you name the &lt;strong&gt;rubric as a first-class artefact&lt;/strong&gt; with version control? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you insist on &lt;strong&gt;structured JSON output&lt;/strong&gt; rather than free-form? — required answer.&lt;/li&gt;
&lt;li&gt;Do you use a &lt;strong&gt;stronger or different-family judge&lt;/strong&gt; than production? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you talk about &lt;strong&gt;judge bias and prompt sensitivity&lt;/strong&gt; as engineering problems, not model flaws? — senior signal.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — a 3-criterion rubric + JSON schema for a support chatbot
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Design the judge for the customer-support chatbot. Rubric: correctness, completeness, style. Model: GPT-4o. Output: JSON with per-criterion scores plus an overall + rationale. Show the exact prompt, the pydantic schema, and a worked scoring on a representative row.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Judge model.&lt;/strong&gt; GPT-4o (temperature=0).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rubric criteria.&lt;/strong&gt; correctness (0.0-1.0), completeness (0.0-1.0), style (0.0-1.0).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overall score.&lt;/strong&gt; Weighted mean: 0.5*correctness + 0.3*completeness + 0.2*style.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output.&lt;/strong&gt; JSON via &lt;code&gt;response_format={"type": "json_object"}&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the judge prompt, the pydantic schema, and the client code. Score a representative row and interpret.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Input&lt;/td&gt;
&lt;td&gt;"How do I cancel my subscription?"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expected&lt;/td&gt;
&lt;td&gt;"You can cancel anytime by opening Settings → Billing → Cancel. The cancellation takes effect at the end of your current billing period."&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prediction&lt;/td&gt;
&lt;td&gt;"You may cancel any time from the billing settings; the change is effective at the end of the current cycle."&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# llm_judge.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ValidationError&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;RUBRIC_HASH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;c93f11&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;     &lt;span class="c1"&gt;# bumps whenever the prompt changes
&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;JudgeBreakdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;correctness&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(...,&lt;/span&gt; &lt;span class="n"&gt;ge&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;le&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;completeness&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(...,&lt;/span&gt; &lt;span class="n"&gt;ge&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;le&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(...,&lt;/span&gt; &lt;span class="n"&gt;ge&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;le&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;JudgeResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(...,&lt;/span&gt; &lt;span class="n"&gt;ge&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;le&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;rationale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;breakdown&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;JudgeBreakdown&lt;/span&gt;


&lt;span class="n"&gt;SYSTEM_PROMPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
You are an expert evaluator for a customer support chatbot. You score the
`prediction` against the `expected` answer on THREE criteria:

- correctness (0.0-1.0): factually accurate; no wrong dates, URLs, entities,
  or polarity flips. If the prediction contradicts the expected answer in
  any material way, correctness is 0.0.
- completeness (0.0-1.0): includes the essential facts from the expected
  answer. Missing a required step, disclaimer, or call-to-action lowers
  completeness proportionally.
- style (0.0-1.0): tone matches the expected style (empathetic where the
  expected is empathetic, formal where the expected is formal); grammar
  and phrasing are natural.

You are BLIND to answer length: a shorter correct answer scores as high as
a longer correct answer. You do NOT reward verbosity.

Return a JSON object with the schema:
{
  &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &amp;lt;weighted mean: 0.5 * correctness + 0.3 * completeness + 0.2 * style&amp;gt;,
  &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rationale&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;= 500 tokens explaining the criterion scores&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,
  &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;breakdown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: {&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;correctness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: ..., &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completeness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: ..., &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;style&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: ...}
}

Do not include any other fields; do not include markdown; return raw JSON.
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;judge_row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;input_text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;prediction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;rubric&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;JudgeResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;user_prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INPUT:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;input_text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EXPECTED:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PREDICTION:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prediction&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;response_format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json_object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SYSTEM_PROMPT&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user_prompt&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;JudgeResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_validate_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;ValidationError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;judge returned invalid JSON: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;judge_row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;input_text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;How do I cancel my subscription?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You can cancel anytime by opening Settings → Billing → Cancel. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The cancellation takes effect at the end of your current billing period.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;prediction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You may cancel any time from the billing settings; the change &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is effective at the end of the current cycle.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_dump&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The system prompt spells out the three criteria with explicit language, calls out the specific failure modes (wrong URLs, polarity flips, missing steps), and tells the judge &lt;em&gt;not&lt;/em&gt; to reward verbosity. This is the rubric-as-SLA discipline in one prompt.&lt;/li&gt;
&lt;li&gt;The output schema is a pydantic model: &lt;code&gt;score&lt;/code&gt; in [0, 1], &lt;code&gt;rationale&lt;/code&gt; string, &lt;code&gt;breakdown&lt;/code&gt; per-criterion. &lt;code&gt;response_format={"type": "json_object"}&lt;/code&gt; forces JSON; &lt;code&gt;temperature=0.0&lt;/code&gt; makes the judgement deterministic; &lt;code&gt;RUBRIC_HASH&lt;/code&gt; versions the prompt.&lt;/li&gt;
&lt;li&gt;On the sample row, the prediction is a paraphrase of the expected: same steps, same effect, slightly different wording. The judge scores correctness at 1.0 (all facts match), completeness at 0.9 (mentions cancel + end-of-cycle, misses the explicit menu path), style at 0.85 (informal but appropriate). Overall = 0.5×1.0 + 0.3×0.9 + 0.2×0.85 = 0.94.&lt;/li&gt;
&lt;li&gt;The rationale is included for auditability. When a PR author sees a per-row miss, they can read the rationale and understand &lt;em&gt;why&lt;/em&gt; — "the prediction says 'billing settings' but the expected says 'Settings → Billing → Cancel'; the menu path is less precise" — turning the metric from a black box into a diagnostic tool.&lt;/li&gt;
&lt;li&gt;The client caches on &lt;code&gt;(input_hash, expected_hash, prediction_hash, rubric_hash, model)&lt;/code&gt;. Deterministic judgement means identical inputs always return identical scores; cache hits are free.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.94&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rationale"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The prediction correctly identifies the cancellation location (billing settings) and confirms the effective date (end of current cycle), matching the factual content of the expected. It omits the specific menu path (Settings → Billing → Cancel) which lowers completeness slightly. Style is informal but appropriate for a support context; the tone is helpful."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"breakdown"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"correctness"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"completeness"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"style"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Every judge call returns structured JSON validated by pydantic; every prompt is version-hashed; every judgement is deterministic (&lt;code&gt;temperature=0&lt;/code&gt;). Free-form judge outputs are unusable as a metric — engineer for structure.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — cross-judge agreement calibration
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Before trusting a judge, measure it against another judge. Take 100 golden-set rows plus their predictions, score with two independent judges (GPT-4o and Claude Opus), and compute agreement. If the two disagree systematically, the rubric needs work.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Judge A.&lt;/strong&gt; GPT-4o with the 3-criterion rubric.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Judge B.&lt;/strong&gt; Claude Opus with the same rubric.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metrics.&lt;/strong&gt; Pearson correlation on continuous scores; Cohen's κ on discretized pass/fail at 0.7.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decision rule.&lt;/strong&gt; r &amp;gt; 0.85 → either judge is fine; r &amp;lt; 0.7 → tighten the rubric or narrow the criteria.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Build the cross-judge calibrator, run it on 100 rows, and interpret. What do you do if r = 0.62?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rows&lt;/td&gt;
&lt;td&gt;100 (from golden set)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Judge A&lt;/td&gt;
&lt;td&gt;GPT-4o&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Judge B&lt;/td&gt;
&lt;td&gt;Claude Opus 4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompt hash&lt;/td&gt;
&lt;td&gt;c93f11 (same for both)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pass threshold&lt;/td&gt;
&lt;td&gt;score ≥ 0.7&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# cross_judge_agreement.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;scipy.stats&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pearsonr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;spearmanr&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;llm_judge&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;judge_row&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;score_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;predictions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Series&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;judge_row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;input_text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expected_output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;prediction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;predictions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;row_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
            &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;row_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;row_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;set_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;row_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cohens_kappa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Series&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Series&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;a_pass&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;b_pass&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# 2x2 confusion
&lt;/span&gt;    &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;tt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(((&lt;/span&gt;&lt;span class="n"&gt;a_pass&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b_pass&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;ff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(((&lt;/span&gt;&lt;span class="n"&gt;a_pass&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b_pass&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;tf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(((&lt;/span&gt;&lt;span class="n"&gt;a_pass&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b_pass&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;ft&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(((&lt;/span&gt;&lt;span class="n"&gt;a_pass&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b_pass&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;p_obs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ff&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
    &lt;span class="n"&gt;p_a_pos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
    &lt;span class="n"&gt;p_b_pos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ft&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
    &lt;span class="n"&gt;p_exp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p_a_pos&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;p_b_pos&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;p_a_pos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;p_b_pos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p_obs&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;p_exp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;p_exp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p_exp&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;golden/support_v3.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()][:&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;predictions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;runs/run_2026-07-04.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;score_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;predictions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;score_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;predictions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-opus-4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;r_pearson&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pearsonr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r_spearman&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;spearmanr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;kappa&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cohens_kappa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Pearson r = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r_pearson&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  (p = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;p1&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Spearman r = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r_spearman&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cohen&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s κ  = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;kappa&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r_pearson&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PASS: either judge is fine; pick the cheaper one for PR sample.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;r_pearson&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.70&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MARGINAL: use the more expensive judge; re-examine rubric criteria.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FAIL: rubric ambiguous. Tighten criteria; re-write style axis; re-run.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Score the same 100 rows through both judges with the identical rubric. Both judges are &lt;code&gt;temperature=0&lt;/code&gt;, so within-judge reruns are stable; the delta between judges is the "rubric ambiguity plus model bias" signal.&lt;/li&gt;
&lt;li&gt;Compute Pearson r (linear correlation of continuous scores) and Spearman r (rank correlation, robust to scale differences). Compute Cohen's κ on the discretized pass/fail at 0.7 — this captures how well the judges agree on the &lt;em&gt;decision&lt;/em&gt; level.&lt;/li&gt;
&lt;li&gt;Interpret: r ≥ 0.85 means the judges agree strongly; either can be used, so pick the cheaper for high-frequency runs. r in [0.7, 0.85) is marginal — the expensive judge should stay in the gate, and the rubric deserves another look. r &amp;lt; 0.7 is a rubric problem: the criteria are ambiguous enough that reasonable judges disagree.&lt;/li&gt;
&lt;li&gt;If r = 0.62, look at the per-row scores where the judges disagree. Common causes: the &lt;code&gt;style&lt;/code&gt; axis is subjective (empathy is culture-dependent), the &lt;code&gt;completeness&lt;/code&gt; axis is underspecified (which facts are required?), or the rubric conflates axes that need to be separated. Rewrite the ambiguous criteria and re-run.&lt;/li&gt;
&lt;li&gt;Once the rubric is stable and r &amp;gt; 0.85, freeze the rubric hash and pin the cheaper judge for the PR sample. The expensive judge stays on the nightly calibration run as an anchor — if the cheap judge starts drifting from the expensive one, the calibration diff flags it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pearson r = 0.891  (p = 0.0000)
Spearman r = 0.867
Cohen's κ  = 0.782
PASS: either judge is fine; pick the cheaper one for PR sample.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Cross-judge calibration is the acceptance test for a rubric. A rubric that two strong judges score inconsistently is a rubric your PR author will not trust. Tighten first, then judge.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the length-bias trap and how to instrument against it
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; LLM judges systematically prefer longer answers even when the shorter answer is factually complete. Instrument the judge output with per-row prediction length, plot score-vs-length, detect bias, and inject an anti-bias instruction into the rubric.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Signal.&lt;/strong&gt; Positive correlation between &lt;code&gt;len(prediction)&lt;/code&gt; and &lt;code&gt;judge_score&lt;/code&gt;, controlling for correctness.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Detection.&lt;/strong&gt; Regress &lt;code&gt;judge_score&lt;/code&gt; on &lt;code&gt;len_pred&lt;/code&gt; while holding &lt;code&gt;expected_correctness&lt;/code&gt; fixed; a positive slope is length bias.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix.&lt;/strong&gt; Add "you are blind to length" language + a paired-example demonstration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Build the length-bias detector against a 300-row scored set, quantify the bias slope, and update the rubric to counteract it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Scored rows&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Judge scores&lt;/td&gt;
&lt;td&gt;judge_score column from prior run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prediction length&lt;/td&gt;
&lt;td&gt;len(prediction) in chars&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Baseline (correct-answer) length&lt;/td&gt;
&lt;td&gt;180 chars mean&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# length_bias_detector.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.linear_model&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;LinearRegression&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;detect_length_bias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scored_df&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    scored_df must have: judge_score (float 0-1), prediction (str), correctness (float 0-1).
    Regresses judge_score on len_pred while conditioning on correctness.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scored_df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;len_pred&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prediction&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;len_pred_z&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;len_pred&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;len_pred&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;len_pred&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;std&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# Partial regression: judge_score ~ len_pred_z + correctness
&lt;/span&gt;    &lt;span class="n"&gt;X&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;len_pred_z&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;correctness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]].&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;
    &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;judge_score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LinearRegression&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;slope_len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;coef_&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;          &lt;span class="c1"&gt;# per-standard-deviation of length
&lt;/span&gt;    &lt;span class="n"&gt;slope_correct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;coef_&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;r_partial&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;corrcoef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;intercept_&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;slope_correct&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;slope_length_per_sd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slope_len&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;slope_correctness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slope_correct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;partial_r_length&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r_partial&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;interpretation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BIAS: judge rewards length&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;slope_len&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.02&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OK: no meaningful length bias&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;runs/run_2026-07-04_scored.parquet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;detect_length_bias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


&lt;span class="c1"&gt;# --- Anti-bias rubric update (append to SYSTEM_PROMPT) ---
&lt;/span&gt;&lt;span class="n"&gt;ANTI_BIAS_ADDENDUM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
CRITICAL: You must be blind to answer length. Two answers that convey the same
factual content should receive the same score, regardless of length. A 40-word
correct answer scores identically to a 200-word correct answer.

Consider this pair:
- Prediction A (short): &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cancel from Settings → Billing → Cancel; effective at cycle end.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;
- Prediction B (long): &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You can absolutely cancel your subscription at any time! To do so, please navigate to Settings, then Billing, then Cancel. The cancellation will take effect at the end of your current billing cycle. If you have any questions, feel free to reach out to our support team...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;

Both should score identically (correctness=1.0, completeness=1.0), because both
contain the same factual information. Prediction B&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s length adds no signal.
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Regress &lt;code&gt;judge_score&lt;/code&gt; on standardized &lt;code&gt;len_pred&lt;/code&gt; while controlling for &lt;code&gt;correctness&lt;/code&gt; (which is itself judge-labelled — a limitation the harness acknowledges). A positive slope on &lt;code&gt;len_pred_z&lt;/code&gt; while &lt;code&gt;correctness&lt;/code&gt; is fixed is length bias by construction.&lt;/li&gt;
&lt;li&gt;On the reference set, the slope typically lands in the range 0.03–0.08 for GPT-4-class judges without an explicit anti-bias instruction — meaning a 1-standard-deviation increase in length (say, +200 chars) raises the judge score by 3–8 points on the 0-100 scale. Significant, and directionally wrong.&lt;/li&gt;
&lt;li&gt;The interpretation triggers a rubric update: append the anti-bias language plus a paired-example demonstration. The demonstration is what actually moves the needle — instructing the judge to "consider two predictions of the same content" is far more effective than telling it to "ignore length."&lt;/li&gt;
&lt;li&gt;After the rubric update, re-run the detector on the same 300 rows. Slope typically drops to 0.005–0.015 — small residual bias, but small enough that the metric is not systematically favoring verbose answers.&lt;/li&gt;
&lt;li&gt;The detector runs on every nightly full-set run. If the slope creeps back up (rubric drift, model update on the judge side), the alert fires and the team re-examines the rubric before the bias corrupts weeks of PR gating.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rubric version&lt;/th&gt;
&lt;th&gt;Slope (length)&lt;/th&gt;
&lt;th&gt;Slope (correctness)&lt;/th&gt;
&lt;th&gt;Interpretation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;v1.0 (no anti-bias)&lt;/td&gt;
&lt;td&gt;0.062&lt;/td&gt;
&lt;td&gt;0.71&lt;/td&gt;
&lt;td&gt;BIAS: judge rewards length&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v1.1 (+ anti-bias)&lt;/td&gt;
&lt;td&gt;0.008&lt;/td&gt;
&lt;td&gt;0.79&lt;/td&gt;
&lt;td&gt;OK: no meaningful length bias&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Judge bias detection is a nightly check, not a one-time calibration. Rubric drift is real; new judge model versions ship weekly; the harness that watches the judge is the harness the team trusts. Never assume the judge stays honest without an instrument.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on the judge design
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Design an LLM-as-judge for a code-explanation assistant where correctness must be exact, style is 'friendly and precise,' and the judge must be robust against style drift when the production model changes. Walk me through the rubric, model choice, output schema, calibration, and the anti-bias instrumentation."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a versioned rubric + stronger-judge + cross-model calibration + length-bias check
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# code_assistant_judge.py — the reference judge for a code-explanation task
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;RUBRIC_HASH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cx-42a9&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;       &lt;span class="c1"&gt;# bump on any prompt edit
&lt;/span&gt;&lt;span class="n"&gt;JUDGE_MODEL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;         &lt;span class="c1"&gt;# production is Claude Sonnet; judge is different family
&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CodeExplainBreakdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;factual_correctness&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(...,&lt;/span&gt; &lt;span class="n"&gt;ge&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;le&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;code_accuracy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(...,&lt;/span&gt; &lt;span class="n"&gt;ge&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;le&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# cited APIs / syntax right
&lt;/span&gt;    &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(...,&lt;/span&gt; &lt;span class="n"&gt;ge&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;le&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CodeExplainResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(...,&lt;/span&gt; &lt;span class="n"&gt;ge&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;le&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;rationale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;breakdown&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CodeExplainBreakdown&lt;/span&gt;


&lt;span class="n"&gt;SYSTEM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
You evaluate a code-explanation assistant. You compare a prediction to a
golden expected answer on THREE criteria:

1. factual_correctness (0.0-1.0): Does the explanation match the expected
   behavior? Wrong function names, wrong parameters, wrong return types, or
   wrong Big-O = 0.0. Partial matches score in between.

2. code_accuracy (0.0-1.0): Are the code snippets in the prediction runnable
   and match the expected API? Wrong imports, wrong syntax, or wrong API
   surface = 0.0. Minor style issues that do not affect runnability = 0.9-1.0.

3. style (0.0-1.0): Is the tone &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;friendly and precise&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;? Precise means using
   the exact technical term (say &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;generator&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; not &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;iterator&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; when generator
   is correct). Friendly means direct, non-condescending, no filler. Verbosity
   is NOT a positive signal.

CRITICAL: You are blind to answer length. Two explanations of the same content
score identically. A 30-word correct answer = a 200-word correct answer.

Return raw JSON:
{
  &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &amp;lt;0.5*factual + 0.3*code + 0.2*style&amp;gt;,
  &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rationale&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;= 400 tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,
  &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;breakdown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: {&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;factual_correctness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: ..., &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code_accuracy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: ..., &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;style&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: ...}
}
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;judge_code_row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prediction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;CodeExplainResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INPUT:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;input_text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;EXPECTED:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;PREDICTION:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prediction&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;JUDGE_MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;response_format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json_object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SYSTEM&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;CodeExplainResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_validate_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="c1"&gt;# --- Nightly instrumentation harness ---
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;nightly_calibration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;predictions&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;judge_code_row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expected_output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;predictions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;row_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]).&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;judge_code_row_with_alt_judge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expected_output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;predictions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;row_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]).&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;scipy.stats&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pearsonr&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;pearsonr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;th&gt;Rationale&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Production model&lt;/td&gt;
&lt;td&gt;Claude Sonnet&lt;/td&gt;
&lt;td&gt;Team's product choice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Judge model&lt;/td&gt;
&lt;td&gt;GPT-4o&lt;/td&gt;
&lt;td&gt;Different family; avoids correlated failures&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rubric criteria&lt;/td&gt;
&lt;td&gt;factual_correctness, code_accuracy, style&lt;/td&gt;
&lt;td&gt;Three axes cover this task&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weights&lt;/td&gt;
&lt;td&gt;0.5 / 0.3 / 0.2&lt;/td&gt;
&lt;td&gt;Factual dominates; code accuracy matters; style is tiebreaker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output schema&lt;/td&gt;
&lt;td&gt;pydantic-validated JSON&lt;/td&gt;
&lt;td&gt;Structured, typed, cacheable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rubric hash&lt;/td&gt;
&lt;td&gt;cx-42a9&lt;/td&gt;
&lt;td&gt;Frozen on merge; bumps invalidate cache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Length bias check&lt;/td&gt;
&lt;td&gt;nightly&lt;/td&gt;
&lt;td&gt;Detects rubric drift&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-judge calibration&lt;/td&gt;
&lt;td&gt;weekly&lt;/td&gt;
&lt;td&gt;Pin agreement r &amp;gt; 0.85&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After the rollout, the judge scores every prediction on three axes with a stable weighted overall, produces per-criterion breakdowns for forensics, and is version-controlled at the rubric level. The nightly length-bias check catches drift; the weekly cross-judge calibration keeps the judge honest against a different-family alternate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Instrument&lt;/th&gt;
&lt;th&gt;Frequency&lt;/th&gt;
&lt;th&gt;Alert threshold&lt;/th&gt;
&lt;th&gt;Owner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Judge score (per PR)&lt;/td&gt;
&lt;td&gt;on merge&lt;/td&gt;
&lt;td&gt;&amp;lt;baseline - 5%&lt;/td&gt;
&lt;td&gt;PR author&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rubric hash&lt;/td&gt;
&lt;td&gt;on rubric edit&lt;/td&gt;
&lt;td&gt;new hash&lt;/td&gt;
&lt;td&gt;Reviewer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Length-bias slope&lt;/td&gt;
&lt;td&gt;nightly&lt;/td&gt;
&lt;td&gt;&amp;gt; 0.02&lt;/td&gt;
&lt;td&gt;ML infra on-call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-judge Pearson r&lt;/td&gt;
&lt;td&gt;weekly&lt;/td&gt;
&lt;td&gt;&amp;lt; 0.85&lt;/td&gt;
&lt;td&gt;ML infra on-call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Judge cost/PR&lt;/td&gt;
&lt;td&gt;on run&lt;/td&gt;
&lt;td&gt;&amp;gt; $3&lt;/td&gt;
&lt;td&gt;Finance ops&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Different-family judge&lt;/strong&gt;&lt;/strong&gt; — Claude Sonnet in production, GPT-4o as judge. Correlated failure modes are the biggest hidden risk with same-family judging; switching families breaks the correlation and gives you an independent view of quality. The 5-10 percent cost premium is worth the independence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Weighted rubric with typed schema&lt;/strong&gt;&lt;/strong&gt; — 0.5 factual + 0.3 code + 0.2 style is task-specific; the weights encode "what matters most for this product." Pydantic validation on receive means malformed judge output surfaces immediately as an error instead of silently poisoning the metric.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Anti-bias instruction with paired example&lt;/strong&gt;&lt;/strong&gt; — telling the judge "you are blind to length" is worth about 3× more with an explicit paired example than without. LLMs are pattern-matchers; showing the pattern beats declaring it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Nightly length-bias check&lt;/strong&gt;&lt;/strong&gt; — the judge is an artefact under active development. Model updates, prompt drift, and rubric edits all reintroduce bias. The nightly detector is the &lt;em&gt;judge's&lt;/em&gt; CI; without it, the harness measuring quality drifts silently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — GPT-4o judge at ~$0.006/row × 300 golden rows = $1.80 per PR. Weekly cross-judge calibration with Claude Opus adds ~$3 per week. Nightly length-bias check is a regression on existing score data — $0. Total judge cost &amp;lt; $100/month for a mature setup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Data analysis&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — data-analysis&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Data-analysis problems on scorer calibration and bias&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/data-analysis" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;Optimization&lt;/span&gt;
&lt;span&gt;Topic — optimization&lt;/span&gt;
&lt;strong&gt;Optimization problems on judge cost and sampling&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/optimization" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  5. CI integration + production monitoring
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Evaluation is a build gate and a production sensor — one signal, two schedulers
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;a mature &lt;code&gt;llm ci&lt;/code&gt; stack runs the same eval harness in two places — on every PR against the golden set (as a build gate that blocks regressions) and on a sample of live production traffic (as a drift detector that alerts on quality decay) — and the frameworks that glue the pieces together are &lt;code&gt;Ragas&lt;/code&gt; for RAG-specific metrics, &lt;code&gt;DeepEval&lt;/code&gt; for pytest-native regression tests, and &lt;code&gt;promptfoo&lt;/code&gt; for lightweight prompt A/B&lt;/strong&gt;. The engineering discipline is exactly the pattern you already run for every other pipeline: same code path, two schedulers, two alerting surfaces.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi4dedzd1w8in5kbcth2y.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi4dedzd1w8in5kbcth2y.jpeg" alt="Iconographic CI + production monitoring diagram — a PR gate on the left with a pass/fail check, a production sampling glyph on the right feeding a trend chart with a regression alert." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four axes of CI-plus-production.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PR gate.&lt;/strong&gt; The harness runs on every PR touching prompts, retrieval, or model config. It scores the change against the golden set, compares to the previous baseline, and blocks the merge if aggregate score drops more than the configured threshold (typically 5 percent).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nightly full-set.&lt;/strong&gt; A cron job runs the full golden set with the stronger judge every 24 hours. Feeds the baseline the PR gate compares to; catches slow drift that PR-level sampling cannot see.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production sampler.&lt;/strong&gt; A sidecar samples 0.5–2 percent of live production conversations, routes them through the same harness (cheap judge + cosine), and writes scores to a rolling metrics table. This is your "did the metric decay in prod even though CI is green?" sensor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alerting.&lt;/strong&gt; Regression alerts (5 percent aggregate drop) page on-call; production drift alerts (24-hour rolling mean drops 5 percent vs last-week baseline) page on-call; per-class alerts (any class drops 10 percent) page on-call. All feed the same runbook.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The PR gate — the anatomy.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trigger.&lt;/strong&gt; GitHub Actions on &lt;code&gt;pull_request&lt;/code&gt; with path filter (&lt;code&gt;prompts/**&lt;/code&gt;, &lt;code&gt;retrieval/**&lt;/code&gt;, &lt;code&gt;model_config/**&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sample.&lt;/strong&gt; 20 percent of the golden set (typically 300-400 rows) for a 30-second wall clock.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Score.&lt;/strong&gt; Cosine + judge-on-miss for cost efficiency; per-row scores land in a metrics table with the PR SHA and golden-set SHA.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compare.&lt;/strong&gt; Load the previous main-branch baseline (last-merged run) and compute the delta. Aggregate delta ≥ -5 percent = pass; less than that = block.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comment.&lt;/strong&gt; Post the per-class delta table into the PR comment. The author sees exactly which class regressed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The nightly full-set — the anatomy.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trigger.&lt;/strong&gt; Cron 02:00 UTC daily. Runs against &lt;code&gt;main&lt;/code&gt; at HEAD.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope.&lt;/strong&gt; Full golden set (2000 rows) with the stronger judge (GPT-4o).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store.&lt;/strong&gt; Metric row per (run_id, golden_set_sha, row_id, judge_score, cosine_score).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Baseline update.&lt;/strong&gt; Aggregate mean over the run becomes the "previous baseline" the next PR run compares to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Report.&lt;/strong&gt; Slack post with per-class means + week-over-week deltas.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The production sampler — the anatomy.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sample.&lt;/strong&gt; 1 percent of live conversations, uniformly random. Higher sample for lower-traffic tenants to keep the metric statistically meaningful.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ground truth.&lt;/strong&gt; For sampled conversations, there is no expected answer. Score cosine against retrieved context and judge against the retrieved answer (RAG-style self-consistency), or use the LLM-judge with a "no reference" rubric ("was this answer useful given the question?").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alert.&lt;/strong&gt; 24-hour rolling mean drops 5 percent vs the same day-of-week last week. Rolling-window baseline handles weekly seasonality.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost.&lt;/strong&gt; ~$50/week for a mid-size product; the cheapest reliable prod-quality sensor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The frameworks — when to reach for each.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ragas.&lt;/strong&gt; Purpose-built for RAG evaluation. Metrics: &lt;code&gt;answer_relevancy&lt;/code&gt;, &lt;code&gt;faithfulness&lt;/code&gt; (does the answer contradict the retrieved context?), &lt;code&gt;context_precision&lt;/code&gt;, &lt;code&gt;context_recall&lt;/code&gt;. Uses LLM-as-judge under the hood. Great starter kit if you're building RAG and don't want to write the judge from scratch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DeepEval.&lt;/strong&gt; pytest-native LLM eval library. Write &lt;code&gt;assert_test(TestCase(input=..., expected=..., metrics=[GEval(...)]))&lt;/code&gt; in your test files; run with &lt;code&gt;pytest&lt;/code&gt;. Fits the "make it a pytest" instinct of most Python teams.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;promptfoo.&lt;/strong&gt; CLI + YAML for prompt A/B. &lt;code&gt;promptfoo eval&lt;/code&gt; runs matrix of prompts × models × inputs and produces an HTML report. Best for lightweight prompt tuning, not production monitoring.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Roll your own.&lt;/strong&gt; Once you have a mature golden set + judge + CI hook, the framework overhead is often not worth it. Ragas and DeepEval win on time-to-first-metric; a hand-rolled harness wins on flexibility once you know exactly what you want.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common CI + production failure modes.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Baseline drift.&lt;/strong&gt; The "previous baseline" is the last-merged score, which can be a lucky high or an unlucky low. Fix: use a 7-day moving average as the baseline, not a single run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flake tolerance.&lt;/strong&gt; A one-time judge hiccup causes a false-fail PR. Fix: retry the harness once on transient judge errors; fail only on the second attempt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Silent judge upgrade.&lt;/strong&gt; OpenAI ships a new GPT-4o checkpoint under the same name; scores shift 2 percent overnight. Fix: pin the judge model to a dated version and log the version on every run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prod sampler skew.&lt;/strong&gt; The 1 percent sample is over-representative of one tenant. Fix: stratified sample per tenant + per topic class.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What senior interviewers listen for.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do you frame CI + prod monitoring as &lt;strong&gt;"same harness, two schedulers"&lt;/strong&gt; rather than as two separate systems? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you volunteer &lt;strong&gt;rolling-baseline&lt;/strong&gt; as the answer to flaky one-run baselines? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you name &lt;strong&gt;Ragas / DeepEval / promptfoo&lt;/strong&gt; and know &lt;em&gt;when&lt;/em&gt; to reach for each? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you distinguish &lt;strong&gt;PR gate (block regression)&lt;/strong&gt; from &lt;strong&gt;production monitor (detect drift)&lt;/strong&gt; as different problems with a shared implementation? — required answer.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — pytest-style regression harness with pass/fail gate
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Build the PR-gate harness as a pytest suite. Each test case is a golden-set row; the metric is the judge score; the gate is aggregate mean vs baseline. The suite runs in the PR CI, blocks the merge on a 5 percent aggregate drop, and posts a per-class delta table into the PR comment.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Test shape.&lt;/strong&gt; One pytest parameterization per row (or one aggregate test with a summary).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Baseline.&lt;/strong&gt; Loaded from a JSON artefact stored in the CI cache, updated on nightly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gate.&lt;/strong&gt; &lt;code&gt;assert mean_score &amp;gt;= baseline - 0.05&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comment.&lt;/strong&gt; Formatted markdown table via &lt;code&gt;gh pr comment&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Implement the pytest harness plus the CI YAML plus the PR comment formatter. Show the exact fail behaviour for a 6 percent aggregate drop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Framework&lt;/td&gt;
&lt;td&gt;pytest + custom fixtures&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Golden set&lt;/td&gt;
&lt;td&gt;400 rows (20% sample of 2000)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Judge&lt;/td&gt;
&lt;td&gt;GPT-4o (temperature=0)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Baseline&lt;/td&gt;
&lt;td&gt;last-nightly aggregate = 0.84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gate threshold&lt;/td&gt;
&lt;td&gt;-0.05 (block if mean &amp;lt; 0.79)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PR result (simulated)&lt;/td&gt;
&lt;td&gt;mean = 0.78 (6 percent drop)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# tests/eval/test_regression.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;statistics&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;harness.load_golden&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_golden&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;harness.predict&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;predict_batch&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;harness.llm_judge&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;judge_row&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;harness.cosine&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;cosine_batch&lt;/span&gt;


&lt;span class="n"&gt;GATE_DELTA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;
&lt;span class="n"&gt;BASELINE_PATH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.ci/eval_baseline.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;golden&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;load_golden&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;golden/support_v3.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;expected_hash&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;b83a11&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;predictions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;golden&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;golden&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;row_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;golden&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;predict_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;


&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;baseline&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BASELINE_PATH&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;BASELINE_PATH&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mean&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;golden&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;
    &lt;span class="n"&gt;rng&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;golden&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;golden&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_aggregate_regression&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;golden&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;predictions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;baseline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;record_property&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;sample&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;golden&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;per_class&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;judge_row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;input_text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expected_output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;prediction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;predictions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;row_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
        &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;
        &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;topic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metadata&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;topic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;per_class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;mean_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;statistics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mean_score&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;baseline&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mean&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;mean_score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;record_property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mean&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mean_score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;record_property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;baseline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;baseline&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mean&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="nf"&gt;record_property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;record_property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;per_class&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;statistics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;per_class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()})&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;baseline&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mean&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;GATE_DELTA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;regression: mean &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;mean_score&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &amp;lt; baseline &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;baseline&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;mean&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; - &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;GATE_DELTA&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/eval-gate.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eval-gate&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;prompts/**'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;retrieval/**'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;model_config/**'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;golden/**'&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;eval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;timeout-minutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.OPENAI_API_KEY }}&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.12'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install -r requirements-eval.txt&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;restore baseline&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/cache@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.ci/eval_baseline.json&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eval-baseline-${{ github.base_ref }}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;run eval suite&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pytest tests/eval/ -q --junitxml=eval-report.xml&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;post PR comment&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python harness/pr_comment.py eval-report.xml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# harness/pr_comment.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;xml.etree.ElementTree&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;ET&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ET&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;getroot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;property&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;mean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mean&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;baseline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n/a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;per_class&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;per_class&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PASS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.05&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BLOCK&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;tbl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;| &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; | &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; |&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;per_class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;### LLM Eval Gate: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;verdict&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;

| metric | value |
|---|---|
| mean | &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; |
| baseline | &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; |
| delta | &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; |
| threshold | -0.050 |

**Per-class means**

| topic | mean |
|---|---|
&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tbl&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;

_Judge: gpt-4o • Rubric: c93f11 • Golden: b83a11_
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;pr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GITHUB_REF&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gh&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;comment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The pytest suite loads the golden set, samples 400 rows deterministically (&lt;code&gt;seed=42&lt;/code&gt;), scores each with the judge, aggregates the mean, and asserts &lt;code&gt;delta &amp;gt;= -0.05&lt;/code&gt; against the baseline. The &lt;code&gt;record_property&lt;/code&gt; calls emit per-run metadata into the JUnit XML for the PR comment step to read.&lt;/li&gt;
&lt;li&gt;The CI workflow triggers only on PRs touching the eval-relevant paths, saving budget when someone edits the README. The &lt;code&gt;actions/cache&lt;/code&gt; step restores the &lt;code&gt;eval_baseline.json&lt;/code&gt; from the base branch's cache; the nightly job is responsible for writing it there.&lt;/li&gt;
&lt;li&gt;When the PR run reports mean=0.78 against baseline 0.84 (a 6 percent drop, exceeding the 5 percent gate), the assertion fails, pytest exits non-zero, GitHub Actions marks the eval-gate check as failed, and the merge is blocked by the branch-protection rule.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;post PR comment&lt;/code&gt; step runs on &lt;code&gt;always()&lt;/code&gt; so a failed run still posts the diagnostic. The comment shows aggregate + per-class means; the author sees "shipping dropped from 0.86 to 0.72, product held at 0.88" and knows immediately where to look.&lt;/li&gt;
&lt;li&gt;Rolling the baseline: the nightly full-set run writes a fresh &lt;code&gt;eval_baseline.json&lt;/code&gt; (aggregated over 2000 rows with the stronger judge) into the CI cache. The next day's PR runs compare against the fresh baseline; the "moving target" concern is bounded because the nightly is deterministic (fixed golden set, fixed judge prompt, temperature 0).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=========================== test session starts ===========================
tests/eval/test_regression.py::test_aggregate_regression FAILED [100%]

================================ FAILURES ================================
______________________ test_aggregate_regression ______________________
&amp;gt; assert delta &amp;gt;= -GATE_DELTA
E    AssertionError: regression: mean 0.780 &amp;lt; baseline 0.840 - 0.05
E    assert -0.06 &amp;gt;= -0.05

------- PR comment posted -------
### LLM Eval Gate: BLOCK

| metric | value |
|---|---|
| mean | 0.780 |
| baseline | 0.840 |
| delta | -0.060 |
| threshold | -0.050 |

Per-class means:
| topic | mean |
|---|---|
| account | 0.815 |
| billing | 0.782 |
| other | 0.821 |
| product | 0.883 |
| shipping | 0.723  ← centre of regression |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; The gate is a &lt;em&gt;pytest&lt;/em&gt; — the same test runner your Python devs use for unit tests. Do not build a bespoke CI system; the moment the harness feels like an isolated framework, the team stops trusting it.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — production sampler with rolling-baseline alert
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Build the production sampler as an Airflow task that runs hourly, pulls a 1 percent stratified sample from the last hour of live conversations, scores each via the same cosine + judge harness, and writes rows into a metrics warehouse table. A downstream alert compares the 24-hour rolling mean against the same-time-last-week baseline and pages on-call if the drop exceeds 5 percent.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sampler.&lt;/strong&gt; Hourly cron; 1 percent stratified by tenant + topic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scoring.&lt;/strong&gt; Cosine against retrieved context + LLM-judge with "no reference" rubric.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage.&lt;/strong&gt; &lt;code&gt;prod_eval_scores(conversation_id, ts, tenant, topic, cosine, judge_score, run_id)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alert.&lt;/strong&gt; 24-hour rolling mean vs same day-of-week last week; &amp;gt; 5% drop = page.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Build the Airflow DAG, the scoring task, and the alert query. Show how the alert fires on a synthetic 6 percent drop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sample rate&lt;/td&gt;
&lt;td&gt;1% stratified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cadence&lt;/td&gt;
&lt;td&gt;hourly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Judge&lt;/td&gt;
&lt;td&gt;gpt-4o-mini (cheap; sample is large)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;Snowflake &lt;code&gt;analytics.prod_eval_scores&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alert query&lt;/td&gt;
&lt;td&gt;24h rolling mean vs same-day last week&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alert threshold&lt;/td&gt;
&lt;td&gt;-5%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# dags/prod_eval_sampler.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DAG&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.decorators&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.providers.snowflake.hooks.snowflake&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SnowflakeHook&lt;/span&gt;

&lt;span class="n"&gt;default_args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;owner&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ml-infra&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retry_delay&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;


&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;DAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;dag_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prod_eval_sampler&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;schedule&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@hourly&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;start_date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;catchup&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;default_args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;default_args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;llm-eval&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prod&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

    &lt;span class="nd"&gt;@task&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sample_conversations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;window_end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;logical_date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;window_start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;window_end&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;hook&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SnowflakeHook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snowflake_conn_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;analytics&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_records&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            WITH candidates AS (
              SELECT conversation_id, tenant, topic, user_turn, model_answer,
                     retrieved_context, ts
              FROM   analytics.conversations
              WHERE  ts &amp;gt;= %s AND ts &amp;lt; %s
            ),
            stratified AS (
              SELECT *,
                     ROW_NUMBER() OVER (PARTITION BY tenant, topic ORDER BY random()) AS rn,
                     COUNT(*) OVER (PARTITION BY tenant, topic) AS n
              FROM   candidates
            )
            SELECT conversation_id, tenant, topic, user_turn, model_answer,
                   retrieved_context, ts
            FROM   stratified
            WHERE  rn &amp;lt;= GREATEST(1, ROUND(n * 0.01));
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;window_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window_end&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="nd"&gt;@task&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;score_and_write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;harness.cosine&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;cosine_batch&lt;/span&gt;
        &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;harness.llm_judge&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;judge_no_reference&lt;/span&gt;

        &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;cos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cosine_batch&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retrieved_context&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model_answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]])[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;judge_no_reference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_turn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retrieved_context&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model_answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversation_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversation_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tenant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tenant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;topic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;topic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cosine&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;judge_score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;

        &lt;span class="n"&gt;hook&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SnowflakeHook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snowflake_conn_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;analytics&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;hook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert_rows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;analytics.prod_eval_scores&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="nf"&gt;score_and_write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sample_conversations&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- alerts/prod_drift.sql — 24h rolling vs same-day last week&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;today&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;judge_score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;mean_today&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prod_eval_scores&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'24 hours'&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;last_week&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;judge_score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;mean_last_week&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prod_eval_scores&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'8 days'&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt;  &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;  &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'7 days'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;today&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mean_today&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;last_week&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mean_last_week&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;today&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mean_today&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;last_week&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mean_last_week&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;CASE&lt;/span&gt;
         &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;today&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mean_today&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;last_week&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mean_last_week&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;05&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'PAGE'&lt;/span&gt;
         &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;today&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mean_today&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;last_week&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mean_last_week&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;03&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'WARN'&lt;/span&gt;
         &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="s1"&gt;'OK'&lt;/span&gt;
       &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;today&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_week&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The sampler queries the last hour of live conversations, stratified 1 percent per &lt;code&gt;(tenant, topic)&lt;/code&gt; pair to prevent one high-volume tenant from dominating the sample. The stratification uses &lt;code&gt;ROW_NUMBER()&lt;/code&gt; + &lt;code&gt;COUNT()&lt;/code&gt; per partition to compute the target sample size in-database.&lt;/li&gt;
&lt;li&gt;Each sampled conversation carries the retrieved context + the model answer. Cosine is computed between context and answer (a proxy for faithfulness — is the answer grounded in what was retrieved?). Judge uses a "no reference" rubric that asks "given the question and the retrieved context, was the answer useful and faithful?"&lt;/li&gt;
&lt;li&gt;Rows land in &lt;code&gt;analytics.prod_eval_scores&lt;/code&gt; with &lt;code&gt;run_id&lt;/code&gt; for lineage. Downstream dashboards slice by tenant, topic, and time; the alert query is a straight aggregation on the same table.&lt;/li&gt;
&lt;li&gt;The alert query compares the 24-hour rolling mean against the same-day-last-week window. The last-week baseline handles weekly seasonality (Monday morning traffic ≠ Sunday evening). A &lt;code&gt;-0.03&lt;/code&gt; warn triggers a Slack post; a &lt;code&gt;-0.05&lt;/code&gt; page fires PagerDuty.&lt;/li&gt;
&lt;li&gt;On the synthetic scenario (today mean=0.78, last week same window=0.85), delta=-0.07 → PAGE. The on-call receives a link to the drift dashboard, the per-tenant breakdown, and the runbook that walks them from "which tenant?" to "which topic?" to "which change landed in the last 7 days?"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;mean_today&lt;/th&gt;
&lt;th&gt;mean_last_week&lt;/th&gt;
&lt;th&gt;delta&lt;/th&gt;
&lt;th&gt;status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0.78&lt;/td&gt;
&lt;td&gt;0.85&lt;/td&gt;
&lt;td&gt;-0.07&lt;/td&gt;
&lt;td&gt;PAGE&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Production monitoring is the &lt;em&gt;same&lt;/em&gt; harness as CI, just pointed at a different data source. Do not build a second scoring stack — build one, run it twice, alert on two schedules.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — using Ragas + DeepEval side by side
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; For teams that want a fast start without building the harness from scratch, &lt;code&gt;Ragas&lt;/code&gt; and &lt;code&gt;DeepEval&lt;/code&gt; are the two most-adopted 2026 frameworks. Ragas is RAG-first with pre-built metrics (faithfulness, answer_relevancy, context_precision, context_recall); DeepEval is pytest-native and general-purpose (any-metric, any-scorer). Use both: Ragas for the RAG-specific axes, DeepEval as the test-runner glue.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ragas metrics.&lt;/strong&gt; &lt;code&gt;faithfulness&lt;/code&gt; (does the answer contradict the context?), &lt;code&gt;answer_relevancy&lt;/code&gt; (does it address the question?), &lt;code&gt;context_precision&lt;/code&gt; (are retrieved chunks relevant?), &lt;code&gt;context_recall&lt;/code&gt; (were the necessary chunks retrieved?).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DeepEval role.&lt;/strong&gt; Wraps Ragas metrics inside pytest test cases; runs on PR; posts pass/fail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When to reach for each.&lt;/strong&gt; Ragas alone for a quick RAG dashboard; DeepEval alone for non-RAG tasks; both together for pytest-native RAG gating.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Wire Ragas metrics into a DeepEval test suite that runs on PR. Show the exact test file, the config, and the failure output on a faithfulness drop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Task&lt;/td&gt;
&lt;td&gt;RAG chatbot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ragas metrics&lt;/td&gt;
&lt;td&gt;faithfulness, answer_relevancy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepEval integration&lt;/td&gt;
&lt;td&gt;GEval wrapper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PR gate&lt;/td&gt;
&lt;td&gt;faithfulness &amp;gt;= 0.85, answer_relevancy &amp;gt;= 0.80&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# tests/eval/test_rag_ragas.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;deepeval&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;assert_test&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;deepeval.metrics&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RagasMetric&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;deepeval.test_case&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;LLMTestCase&lt;/span&gt;


&lt;span class="n"&gt;GOLDEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;golden/rag_support_v3.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;


&lt;span class="n"&gt;faithfulness_metric&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RagasMetric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;faithfulness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;relevancy_metric&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RagasMetric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer_relevancy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@pytest.mark.parametrize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;row&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;golden/rag_support_v3.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_ragas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;tc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LLMTestCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;actual_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prediction&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;expected_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expected_output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;retrieval_context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retrieved_context&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;assert_test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;faithfulness_metric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;relevancy_metric&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/rag-eval.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rag-eval&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pull_request&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ragas-deepeval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.OPENAI_API_KEY }}&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.12'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install deepeval ragas&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pytest tests/eval/test_rag_ragas.py -q --html=rag-eval.html --self-contained-html&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rag-eval-report&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rag-eval.html&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;LLMTestCase&lt;/code&gt; from DeepEval carries the four fields Ragas needs: &lt;code&gt;input&lt;/code&gt; (the user question), &lt;code&gt;actual_output&lt;/code&gt; (the model's answer), &lt;code&gt;expected_output&lt;/code&gt; (the golden), &lt;code&gt;retrieval_context&lt;/code&gt; (the chunks the RAG retrieved). Ragas metrics compute against these fields.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RagasMetric&lt;/code&gt; wraps Ragas's &lt;code&gt;faithfulness&lt;/code&gt; and &lt;code&gt;answer_relevancy&lt;/code&gt; metrics with a DeepEval-compatible &lt;code&gt;threshold&lt;/code&gt; and a scoring &lt;code&gt;model&lt;/code&gt; (GPT-4o). &lt;code&gt;assert_test&lt;/code&gt; runs the metrics and raises pytest failures if any threshold is missed.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;@pytest.mark.parametrize&lt;/code&gt; fans out one test per row. Pytest runs them in parallel with &lt;code&gt;-n auto&lt;/code&gt;; a 500-row set completes in ~2 minutes at 20-way parallelism.&lt;/li&gt;
&lt;li&gt;On a faithfulness regression (say, a retrieval index change that surfaces less-relevant chunks), the faithfulness metric drops on many rows; DeepEval fails the tests; the HTML report shows exactly which rows fell below 0.85. The PR is blocked.&lt;/li&gt;
&lt;li&gt;Ragas + DeepEval is the fastest path from "we have a RAG chatbot" to "we have a gated CI harness" — typically a day of work vs a week for hand-rolled. The trade-off is less flexibility on the metric side; once you outgrow the built-in metrics, migrate to a hand-rolled harness with the same pytest wiring.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tests/eval/test_rag_ragas.py::test_ragas[row-0] PASSED
tests/eval/test_rag_ragas.py::test_ragas[row-1] PASSED
tests/eval/test_rag_ragas.py::test_ragas[row-2] FAILED

    faithfulness = 0.72 (threshold 0.85)
    answer_relevancy = 0.88 (threshold 0.80)

    Failure reason: the answer contained a URL not present in the retrieved
    context — hallucinated citation.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Ragas + DeepEval is the "buy" decision; hand-rolled is the "build" decision. Buy when time-to-first-metric matters more than metric flexibility. Migrate to build when the built-in metrics don't cover a task-specific axis.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on CI + production monitoring
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "You join a team with a green LLM eval dashboard but customers complaining about quality. Walk me through the first two weeks — how you'd diagnose the CI-vs-prod gap, what monitoring you'd add, and how you'd close the loop."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a "same harness, two schedulers, three alerts" hardening plan
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# hardening-plan.yaml — 2 weeks from "CI green, customers unhappy" to closed-loop eval&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1.0&lt;/span&gt;

&lt;span class="na"&gt;week_1&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;day_1&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;audit golden set&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;does it reflect real user turns?&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if not&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sample 200 rows from real live conversations, SME-relabel&lt;/span&gt;
  &lt;span class="na"&gt;day_2&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;stand up production sampler (1% stratified, hourly Airflow task)&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;deploy prod scoring&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cosine + cheap judge, no-reference rubric&lt;/span&gt;
  &lt;span class="na"&gt;day_3&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;build drift alert query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;24h rolling vs same-day last week&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;integrate to PagerDuty; test with historical data&lt;/span&gt;
  &lt;span class="na"&gt;day_4&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;retrofit PR gate baseline&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;switch from single-run to 7-day moving avg&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;single-run baseline is flake-prone; moving avg is stable&lt;/span&gt;
  &lt;span class="na"&gt;day_5&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;retrofit per-class alerts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;any class drops 10% = page (finer signal)&lt;/span&gt;
  &lt;span class="na"&gt;weekend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;soak&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;watch the metrics; expect ~2 alerts (real signal + calibration noise)&lt;/span&gt;

&lt;span class="na"&gt;week_2&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;day_1&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;reconcile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;for the top-3 customer-reported issues, is the golden set covering them?&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if not&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;add rows in the missing class + retrain the SME labelers&lt;/span&gt;
  &lt;span class="na"&gt;day_2&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cross-judge calibration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;run 100 rows through GPT-4o and Claude Opus&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if r &amp;lt; 0.85&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tighten rubric; publish updated hash&lt;/span&gt;
  &lt;span class="na"&gt;day_3&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;length-bias detector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;run against last 30 days of judge scores&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if bias &amp;gt; 0.02&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;append anti-bias language; re-baseline&lt;/span&gt;
  &lt;span class="na"&gt;day_4&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Ragas / DeepEval evaluation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;does the built-in RAG suite catch what our custom does not?&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;integrate the specific Ragas metric that catches a real failure&lt;/span&gt;
  &lt;span class="na"&gt;day_5&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;runbook + on-call rotation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eval-gate + prod-drift on the same paged rotation&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Day&lt;/th&gt;
&lt;th&gt;Activity&lt;/th&gt;
&lt;th&gt;Signal added&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Golden-set audit&lt;/td&gt;
&lt;td&gt;Coverage of real turns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Prod sampler&lt;/td&gt;
&lt;td&gt;Continuous prod metric&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Drift alert&lt;/td&gt;
&lt;td&gt;Weekly-seasonality-aware&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Rolling baseline&lt;/td&gt;
&lt;td&gt;Flake-resistant PR gate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Per-class alerts&lt;/td&gt;
&lt;td&gt;Localized regression detection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Week 2&lt;/td&gt;
&lt;td&gt;Judge calibration + bias + framework fill-ins&lt;/td&gt;
&lt;td&gt;Judge trustworthiness&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After two weeks, the team has: (1) a golden set that reflects real user turns, (2) a production sampler catching drift on a 24-hour horizon, (3) a PR gate with a stable rolling baseline, (4) per-class alerts for localized regressions, and (5) a calibrated judge with instrumented length-bias detection. The CI-vs-prod gap closes; customer complaints line up with metric drops.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Golden set reflects real turns&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production monitoring&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;1% hourly sample&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PR baseline&lt;/td&gt;
&lt;td&gt;last-run&lt;/td&gt;
&lt;td&gt;7-day moving avg&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-class alerts&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;10% drop = page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-judge calibration&lt;/td&gt;
&lt;td&gt;never&lt;/td&gt;
&lt;td&gt;weekly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Length bias monitored&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;nightly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI-prod gap&lt;/td&gt;
&lt;td&gt;large&lt;/td&gt;
&lt;td&gt;closed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Same harness, two schedulers&lt;/strong&gt;&lt;/strong&gt; — the PR gate and the prod monitor share code paths. Debugging is easier because a bug in the harness surfaces in both places at once; the team is not maintaining two scoring stacks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Rolling baseline&lt;/strong&gt;&lt;/strong&gt; — 7-day moving average absorbs individual-run noise. A single unlucky run does not lock the team out of every future PR; a real regression persists through the moving average and still triggers the gate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Stratified prod sampler&lt;/strong&gt;&lt;/strong&gt; — 1 percent per (tenant, topic) prevents a single high-volume tenant from dominating. The metric is representative of the full traffic distribution, not just of the largest customer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cross-judge calibration + length-bias check&lt;/strong&gt;&lt;/strong&gt; — the judge is a moving target too. Weekly cross-model calibration keeps the judge honest; nightly length-bias check catches rubric drift before it corrupts weeks of gating.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — 2 senior-engineer weeks + ~$200/month in judge + prod-sampler API cost. The alternative (unbounded customer complaints + eng escalation) is measured in engineering-days per week; the eval stack pays for itself the first time it catches a bad deploy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;ETL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — etl&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;ETL problems on CI harnesses and drift monitoring&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;


&lt;p&gt;&lt;span&gt;Real-time analytics&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — real-time-analytics&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Real-time analytics problems on rolling baselines and alerts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/real-time-analytics" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;





&lt;h2&gt;
  
  
  Cheat sheet — LLM eval recipes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Golden set YAML template.&lt;/strong&gt; &lt;code&gt;path: golden/support_v3.jsonl&lt;/code&gt; + &lt;code&gt;version: v3.2-sha-e1f2c9&lt;/code&gt; + &lt;code&gt;rows: 300&lt;/code&gt; + &lt;code&gt;class_balance: {billing: 60, account: 60, product: 60, shipping: 60, other: 60}&lt;/code&gt; + &lt;code&gt;growth: +20 rows/week&lt;/code&gt; + &lt;code&gt;curation: SME-labelled + LLM-augmented&lt;/code&gt;. Every row is &lt;code&gt;(input, expected_output, metadata)&lt;/code&gt; where &lt;code&gt;metadata&lt;/code&gt; carries &lt;code&gt;topic&lt;/code&gt;, &lt;code&gt;difficulty&lt;/code&gt;, &lt;code&gt;language&lt;/code&gt;, &lt;code&gt;source_ticket_id&lt;/code&gt;, &lt;code&gt;sme_labeler&lt;/code&gt;, &lt;code&gt;reviewer&lt;/code&gt;, &lt;code&gt;retrieval_snapshot_id&lt;/code&gt;, &lt;code&gt;forbidden_patterns&lt;/code&gt;, &lt;code&gt;style_tag&lt;/code&gt;. Store as JSONL in git; content-hash on canonicalized JSON; tag per release; verify hash on load.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cosine scorer 20-line skeleton.&lt;/strong&gt; Pin embedding: &lt;code&gt;text-embedding-3-large@2024-11-01&lt;/code&gt;. Batch 100 at a time; L2-normalize; dot product for cosine. Threshold per language (en=0.86, es/fr=0.83); calibrate against 200 correct + 200 contradiction pairs; sweep F1 or cost-weighted. Judge-on-miss to save cost. Every row emits &lt;code&gt;(row_id, topic, cosine, cos_pass, judge_score, final_pass, embedding_version)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM-as-judge prompt template.&lt;/strong&gt; System: explicit 2-4 criterion rubric with &lt;code&gt;(0.0-1.0)&lt;/code&gt; scale + failure modes named + "you are blind to length" + paired-example demonstration. User: &lt;code&gt;INPUT: ... EXPECTED: ... PREDICTION: ...&lt;/code&gt;. Response format: &lt;code&gt;{"type": "json_object"}&lt;/code&gt;; temperature 0; pydantic-validate on receive. Weighted overall score, e.g. &lt;code&gt;0.5*correctness + 0.3*completeness + 0.2*style&lt;/code&gt;. Hash the prompt; bump on every edit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Judge model choice.&lt;/strong&gt; Use a different-family model from production (GPT-4o judge for Claude Sonnet prod, Claude Opus judge for GPT-4o prod). Cheap sample judge (gpt-4o-mini) on PR + strong nightly judge (gpt-4o or claude-opus) for calibration. Cross-judge Pearson r ≥ 0.85 before trusting either.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pytest regression harness.&lt;/strong&gt; &lt;code&gt;assert_test(TestCase(input=..., expected=..., metrics=[GEval(...)]))&lt;/code&gt; with DeepEval or a hand-rolled &lt;code&gt;pytest.mark.parametrize&lt;/code&gt;. Gate: &lt;code&gt;mean &amp;gt;= baseline - 0.05&lt;/code&gt; against 7-day moving-average baseline. Post per-class delta table into PR via &lt;code&gt;gh pr comment&lt;/code&gt;. Cache the baseline via &lt;code&gt;actions/cache&lt;/code&gt; keyed on base branch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI eval workflow.&lt;/strong&gt; Trigger: &lt;code&gt;pull_request&lt;/code&gt; on &lt;code&gt;prompts/**&lt;/code&gt;, &lt;code&gt;retrieval/**&lt;/code&gt;, &lt;code&gt;model_config/**&lt;/code&gt;, &lt;code&gt;golden/**&lt;/code&gt;. Sample 20 percent of the golden set (~400 rows) for the PR run; nightly full-set with the stronger judge. Timeout 5-10 minutes; parallelism 20; retry once on transient judge errors. Post pass/fail comment always.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production sampler.&lt;/strong&gt; Hourly Airflow task; 1 percent stratified sample per &lt;code&gt;(tenant, topic)&lt;/code&gt; via &lt;code&gt;ROW_NUMBER() OVER (PARTITION BY tenant, topic ORDER BY random())&lt;/code&gt;. Score with cheap judge (&lt;code&gt;gpt-4o-mini&lt;/code&gt;) + cosine on retrieved context. Write to &lt;code&gt;prod_eval_scores(conversation_id, ts, tenant, topic, cosine, judge_score, run_id)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drift alert query.&lt;/strong&gt; &lt;code&gt;24h_rolling_mean - same_day_last_week_mean &amp;lt;= -0.05 → PAGE&lt;/code&gt;, &lt;code&gt;&amp;lt;= -0.03 → WARN&lt;/code&gt;. Use &lt;code&gt;INTERVAL '8 days'&lt;/code&gt; to &lt;code&gt;INTERVAL '7 days'&lt;/code&gt; for same-day-last-week baseline (handles weekly seasonality). Alert on per-class rolling means too — any class drops 10 percent = page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sizing the eval budget.&lt;/strong&gt; &lt;code&gt;annual_cost = golden_set_size × judge_cost_per_row × runs_per_week × 52&lt;/code&gt;. Levers: sample 20 percent on PR + full nightly (cuts 5×); cheap judge on sample + expensive nightly (cuts another 5×); reduce sensitive runs to weekly (cuts another 5×). Target: total eval &amp;lt; 5 percent of the LLM-feature's production inference bill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-judge calibration.&lt;/strong&gt; Weekly: score 100 rows through two judges (different families), compute Pearson r + Cohen's κ at 0.7. r ≥ 0.85 → pass; r in [0.7, 0.85) → tighten rubric; r &amp;lt; 0.7 → rubric is broken. Log the r value; alert on deterioration week-over-week.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Length-bias detector.&lt;/strong&gt; Nightly regression of &lt;code&gt;judge_score&lt;/code&gt; on standardized &lt;code&gt;len(prediction)&lt;/code&gt; while controlling for &lt;code&gt;correctness&lt;/code&gt;. Slope &amp;gt; 0.02 = biased. Fix: append "you are blind to length" plus a paired example (short + long same-content predictions scored identically). Re-run detector; slope should drop below 0.01.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ragas / DeepEval / promptfoo — when to reach for each.&lt;/strong&gt; Ragas: RAG-specific metrics (&lt;code&gt;faithfulness&lt;/code&gt;, &lt;code&gt;answer_relevancy&lt;/code&gt;, &lt;code&gt;context_precision&lt;/code&gt;, &lt;code&gt;context_recall&lt;/code&gt;) with judge under the hood — best when you need a fast RAG dashboard. DeepEval: pytest-native, wraps any metric — best when your team already writes pytest. promptfoo: CLI matrix runner for prompt × model × input — best for lightweight prompt A/B, not production monitoring. Hand-rolled: once you outgrow built-in metrics, roll your own with the same pytest wiring.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt versioning discipline.&lt;/strong&gt; Every prompt (production, judge, RAG-retrieval rewrite) is a hashed artefact in git. Every metric row carries the &lt;code&gt;prompt_hash&lt;/code&gt;; comparing across hashes is disallowed. Prompt edits go through PR review; the eval-gate runs against the new hash before merge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failure modes catalogue.&lt;/strong&gt; Golden set drift (stagnant set diverges from prod distribution) → weekly production sampling into curation queue. Embedding version drift (silent OpenAI checkpoint change) → pin embedding to dated version. Judge bias creep (length preference returns after prompt edit) → nightly length-bias detector. Baseline flake (single unlucky run) → 7-day moving-average baseline. Prompt hash mismatch (rubric edit not propagated) → harness verifies hash on load and aborts on mismatch.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is LLM-as-judge?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;llm as judge&lt;/code&gt; is the pattern where a strong LLM (typically GPT-4o, Claude Opus, or Gemini Ultra) scores your production model's prediction against a golden expected answer using an explicit rubric, returning a structured JSON with a numeric score plus a rationale. Concretely, the judge sees &lt;code&gt;(input, expected_output, prediction)&lt;/code&gt; and a rubric like &lt;code&gt;["correctness", "completeness", "style"]&lt;/code&gt;, and returns &lt;code&gt;{"score": 0.87, "rationale": "…", "breakdown": {...}}&lt;/code&gt;. It is the dominant modern LLM-eval metric because it handles semantic equivalence (paraphrases score high) and semantic opposition (contradictions score low) far better than cosine similarity or BLEU. The engineering discipline that makes it work is: version-controlled rubric, structured JSON output (&lt;code&gt;response_format={"type": "json_object"}&lt;/code&gt;), &lt;code&gt;temperature=0&lt;/code&gt; for reproducibility, and a stronger or different-family judge model than production to avoid correlated failure modes. Judge cost per row is the finance-visible constraint — typically $0.001-0.01 per row depending on model + rubric length, so sample-on-PR + full-nightly is the default cost-control pattern.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cosine similarity vs LLM-as-judge — which do I use for what?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;cosine similarity&lt;/code&gt; is the cheap, fast, deterministic &lt;em&gt;first-pass&lt;/em&gt; metric: embed both the expected and the prediction with a pinned embedding model (&lt;code&gt;text-embedding-3-large&lt;/code&gt; is the 2026 default), L2-normalize, dot-product, threshold. It costs a fraction of a cent per row, catches structural hallucinations (wrong entities, wrong URLs, missing named entities) beautifully, and is the right gate for "did we regress structurally by 10 percent aggregate." &lt;strong&gt;But it fails on paraphrase (semantically identical but different surface form) and contradiction (opposite polarity but similar surface form) — a "yes" and a "no" can embed similarly, and the metric silently misses the polarity flip.&lt;/strong&gt; &lt;code&gt;llm as judge&lt;/code&gt; is the truth arbiter for those cases: expensive but semantically correct. The mature pipeline runs cosine first (per-row, on every row), escalates to the judge only on cosine misses (a "judge-on-miss" pattern that cuts judge cost by ~7×), and uses both scores in the aggregate. Never use cosine alone as a truth metric; never use judge alone as a first-pass filter — the price-performance sweet spot is the cascade.&lt;/p&gt;

&lt;h3&gt;
  
  
  How big should my golden set be?
&lt;/h3&gt;

&lt;p&gt;The floor is 200 rows for a mid-complexity task; the working default is 300-500; regression-forensics teams run at 1000-2000. &lt;strong&gt;The math:&lt;/strong&gt; at N=20 rows with per-row score standard deviation of ~0.08, the two-sided 95 percent confidence half-width is 0.035 — meaning any regression under 3.5 percent is indistinguishable from noise. At N=200 the half-width drops to 0.011 — a 5 percent regression is caught with a 4× margin, and per-class slices (10-topic buckets × 20 rows each) have enough rows for statistical power. &lt;strong&gt;The bigger reason&lt;/strong&gt; is class coverage: a 20-row demo set has no coverage at all, so localized regressions in unrepresented classes vanish silently; a 300-row balanced set with 5 topic classes × 60 rows each catches localized issues before they ship. &lt;strong&gt;Growth cadence:&lt;/strong&gt; +20 rows/week from production sampling with SME review. &lt;strong&gt;Warning sign:&lt;/strong&gt; if you cannot slice your metric by topic / difficulty / language and get a statistically meaningful number, your golden set is too small.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need to re-evaluate on every prompt change?
&lt;/h3&gt;

&lt;p&gt;Yes, on every PR that touches prompts, retrieval config, model config, or the golden set itself. Prompts are the highest-leverage lever on LLM behaviour; a two-word rewording of a system prompt can silently reshape entire answer classes. &lt;strong&gt;The mature pattern:&lt;/strong&gt; PR gate runs a 20 percent sample (~400 rows) with the cheap judge in ~30 seconds and blocks the merge on a 5 percent aggregate drop; nightly full-set (2000 rows) with the stronger judge recalibrates the baseline. Skipping the PR gate means the &lt;em&gt;first&lt;/em&gt; time you notice a bad prompt is when a customer opens a support ticket. &lt;strong&gt;Cost model:&lt;/strong&gt; for a 400-row PR sample with a &lt;code&gt;gpt-4o-mini&lt;/code&gt; judge at ~$0.006/row, each PR eval costs ~$2.40. At 40 PRs per week that is ~$5,000/year — a rounding error next to the cost of a single production incident. &lt;strong&gt;Exception:&lt;/strong&gt; hotfix prompts that touch a well-understood, narrow behaviour can bypass the gate with an explicit override + a manual eval commitment within 24 hours. Never let the exception become the rule.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I detect LLM regression in production?
&lt;/h3&gt;

&lt;p&gt;Stand up the same eval harness against a 1 percent stratified sample of live traffic, run hourly via Airflow / Dagster / Prefect, write per-conversation scores to a warehouse table, and alert on a 24-hour rolling mean drop vs same-day-last-week. &lt;strong&gt;Stratify&lt;/strong&gt; by &lt;code&gt;(tenant, topic)&lt;/code&gt; so a single high-volume tenant does not dominate. &lt;strong&gt;Score&lt;/strong&gt; with the same cheap judge you use on the PR sample, so the production and CI metrics are directly comparable. &lt;strong&gt;Alert&lt;/strong&gt; at two thresholds: &lt;code&gt;-3 percent = WARN&lt;/code&gt; (Slack post); &lt;code&gt;-5 percent = PAGE&lt;/code&gt; (PagerDuty). &lt;strong&gt;Add per-class alerts&lt;/strong&gt; on any topic dropping more than 10 percent — localized regressions hide inside a healthy-looking aggregate. &lt;strong&gt;The reference alert query&lt;/strong&gt; compares &lt;code&gt;AVG(judge_score) WHERE ts &amp;gt;= NOW() - INTERVAL '24 hours'&lt;/code&gt; against &lt;code&gt;AVG(judge_score) WHERE ts &amp;gt;= NOW() - INTERVAL '8 days' AND ts &amp;lt; NOW() - INTERVAL '7 days'&lt;/code&gt; — the last-week baseline handles weekly traffic seasonality. &lt;strong&gt;Cost:&lt;/strong&gt; ~$50/week for a mid-size product; the cheapest reliable prod-quality sensor.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I use Ragas / DeepEval / promptfoo or roll my own harness?
&lt;/h3&gt;

&lt;p&gt;Start with a framework — either Ragas (RAG-first with built-in &lt;code&gt;faithfulness&lt;/code&gt;, &lt;code&gt;answer_relevancy&lt;/code&gt;, &lt;code&gt;context_precision&lt;/code&gt;, &lt;code&gt;context_recall&lt;/code&gt; metrics) or DeepEval (pytest-native, general-purpose, wraps Ragas + any other metric). &lt;strong&gt;Ragas alone&lt;/strong&gt; for a fast RAG dashboard when you have not yet decided on the judge / harness plumbing. &lt;strong&gt;DeepEval alone&lt;/strong&gt; for non-RAG tasks (classification, extraction, summarization) where the built-in metrics fit. &lt;strong&gt;Both together&lt;/strong&gt; for pytest-native RAG gating — DeepEval's &lt;code&gt;LLMTestCase&lt;/code&gt; + Ragas's &lt;code&gt;RagasMetric&lt;/code&gt; in a pytest file, gated by &lt;code&gt;assert_test&lt;/code&gt;. &lt;strong&gt;promptfoo&lt;/strong&gt; is CLI + YAML for prompt A/B — great for lightweight tuning, not production monitoring. &lt;strong&gt;Roll your own&lt;/strong&gt; once you have (a) a mature golden set, (b) a task-specific rubric that no framework's built-in metric covers, and (c) a team comfortable maintaining Python infrastructure. Migration path from framework → hand-rolled is easy: keep the pytest wiring; swap the metric implementations from &lt;code&gt;RagasMetric&lt;/code&gt; to your own scorer. The one thing not to do: build a bespoke non-pytest harness first — nobody trusts a system they cannot run locally with &lt;code&gt;pytest&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practice on PipeCode
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Drill the &lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;ETL practice library →&lt;/a&gt; for the golden-set curation, versioned dataset, and pipeline-scheduler problems senior interviewers love.&lt;/li&gt;
&lt;li&gt;Rehearse on the &lt;a href="https://pipecode.ai/explore/practice/topic/data-analysis" rel="noopener noreferrer"&gt;data-analysis practice library →&lt;/a&gt; for the class-balance, threshold-calibration, and per-slice-metric problems that motivate the LLM-eval harness in the first place.&lt;/li&gt;
&lt;li&gt;Sharpen the tuning axis with the &lt;a href="https://pipecode.ai/explore/practice/topic/optimization" rel="noopener noreferrer"&gt;optimization practice library →&lt;/a&gt; for the eval-cost, judge-sampling, and CI-throughput problems that decide whether the harness scales.&lt;/li&gt;
&lt;li&gt;Stack the prerequisites against PipeCode's broader 450+ data-engineering catalogue to anchor the golden-set + judge + CI intuition against real graded inputs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Lock in LLM-evaluation muscle memory&lt;/h3&gt;

&lt;p&gt;Framework docs explain metrics. PipeCode drills explain the decision — when cosine is enough, when the judge must kick in, when the golden set is too small to trust, when the PR gate is the wrong place to catch drift. &lt;a href="https://pipecode.ai/" rel="noopener noreferrer"&gt;Pipecode.ai&lt;/a&gt; is Leetcode for Data Engineering — pattern-first practice tuned for the production trade-offs senior data engineers actually face.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;Practice ETL problems →&lt;/a&gt;&lt;br&gt;
&lt;a href="https://pipecode.ai/explore/practice/topic/data-analysis" rel="noopener noreferrer"&gt;Practice data-analysis problems →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>sql</category>
      <category>interview</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Embeddings Refresh Pipelines: Incremental Updates, Cost, Drift Monitoring</title>
      <dc:creator>Gowtham Potureddi</dc:creator>
      <pubDate>Fri, 10 Jul 2026 16:33:53 +0000</pubDate>
      <link>https://dev.to/gowthampotureddi/embeddings-refresh-pipelines-incremental-updates-cost-drift-monitoring-32dd</link>
      <guid>https://dev.to/gowthampotureddi/embeddings-refresh-pipelines-incremental-updates-cost-drift-monitoring-32dd</guid>
      <description>&lt;p&gt;&lt;strong&gt;&lt;code&gt;embeddings refresh&lt;/code&gt;&lt;/strong&gt; is the quiet operational discipline that separates a RAG prototype from a production embeddings pipeline — and the one component senior data engineers keep discovering is missing on day one of an "our semantic search results are getting worse" incident. Every product ships retrieval-augmented generation or semantic search in 2026; every one of those products backs onto a vector store — pgvector, Pinecone, Weaviate, Milvus — that is a &lt;em&gt;stateful asset&lt;/em&gt; on the same footing as the warehouse. The moment the source corpus changes, the vectors decay; the moment the embedding model version rolls forward, the whole space rotates; the moment the OpenAI bill lands, someone in finance asks why a &lt;code&gt;vector refresh&lt;/code&gt; cost 12,000 dollars this month. The engineering trade-off lives in &lt;em&gt;which change-detection strategy you pick&lt;/em&gt;, &lt;em&gt;which cost tier you batch into&lt;/em&gt;, and &lt;em&gt;which drift metric you alert on&lt;/em&gt; — not in whether refresh matters.&lt;/p&gt;

&lt;p&gt;This guide is the senior-DE walkthrough you wished existed the first time an interviewer asked "how do you re-embed a 10-million-document corpus without paying full price on the OpenAI dashboard?", "what is your &lt;code&gt;incremental embedding&lt;/code&gt; strategy when only 2% of rows change per day?", or "how do you monitor &lt;code&gt;embedding drift&lt;/code&gt; before your product manager files a bug about relevance?". It walks through the four axes senior interviewers actually probe — change detection, cost, model versioning, drift monitoring — the SHA-256 content-hash pattern that turns a full-corpus re-embed into a 2% incremental job, the OpenAI &lt;code&gt;text-embedding-3&lt;/code&gt; and Cohere pricing math that decides between managed API and self-hosted BGE/E5, the &lt;code&gt;pgvector refresh&lt;/code&gt; column layout with a per-row &lt;code&gt;model_tag&lt;/code&gt; that makes migrations reversible, and the recall@k golden-query monitor that catches &lt;code&gt;embedding drift&lt;/code&gt; before the retrieval-quality graph hits the pager. Each section pairs a teaching block with a Solution-Tail interview answer — code, a step-by-step trace, an output table, then a concept-by-concept breakdown of why it works.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx82osk2se6b8jgi68hv8.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx82osk2se6b8jgi68hv8.jpeg" alt="PipeCode blog header for embeddings refresh — bold white headline 'Embeddings Refresh' with subtitle 'Incremental · Cost · Drift' over a document scroll converging with a vector cube onto a purple wax seal reading REFRESH, on a dark gradient with purple, orange, green, and blue accents and a small pipecode.ai attribution." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you want &lt;strong&gt;hands-on reps&lt;/strong&gt; immediately after reading, drill the &lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;SQL practice library →&lt;/a&gt;, rehearse on the &lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;ETL practice library →&lt;/a&gt;, and sharpen the tuning axis with the &lt;a href="https://pipecode.ai/explore/practice/topic/optimization" rel="noopener noreferrer"&gt;optimization practice library →&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;On this page&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why embedding refresh became a first-class data-eng problem&lt;/li&gt;
&lt;li&gt;Change detection + incremental refresh&lt;/li&gt;
&lt;li&gt;Cost accounting — dollars per doc&lt;/li&gt;
&lt;li&gt;Model versioning + migration&lt;/li&gt;
&lt;li&gt;Drift monitoring + freshness&lt;/li&gt;
&lt;li&gt;Cheat sheet — embeddings refresh recipes&lt;/li&gt;
&lt;li&gt;Frequently asked questions&lt;/li&gt;
&lt;li&gt;Practice on PipeCode&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1. Why embedding refresh became a first-class data-eng problem
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Every embedding is a snapshot — the moment content changes or the model rolls forward, your vectors go stale
&lt;/h3&gt;

&lt;p&gt;The one-sentence invariant: &lt;strong&gt;an embedding is a lossy, model-versioned function of a piece of content, so the pair &lt;code&gt;(content_version, model_version)&lt;/code&gt; uniquely determines the vector — change either half and the vector on disk is stale&lt;/strong&gt;. Nothing about vector stores makes this obvious. A pgvector row looks like an ordinary column; a Pinecone upsert looks like a write to a key-value store. But every vector is really a cache of &lt;code&gt;f(content, model)&lt;/code&gt;, and every cache needs a refresh policy. Ignore this, and the RAG system quietly rots — user queries return yesterday's answer, then last month's answer, then answers about products the company no longer sells.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four "must-answer" axes interviewers actually probe.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Incremental refresh.&lt;/strong&gt; How do you find the 2% of documents that changed since the last run, embed only those, and leave the rest alone? A senior interviewer expects a content-hash + change-queue answer, not "we re-embed everything nightly." Full-corpus re-embed on a 10M-doc catalogue is 6-figure-a-year money on OpenAI pricing; the incremental strategy is &lt;em&gt;the&lt;/em&gt; cost lever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost accounting.&lt;/strong&gt; What does &lt;code&gt;embedding cost&lt;/code&gt; per document actually work out to on &lt;code&gt;text-embedding-3-small&lt;/code&gt; versus &lt;code&gt;text-embedding-3-large&lt;/code&gt; versus a self-hosted BGE or E5 encoder? The senior signal is naming batch pricing (50% off), the crossover point where self-hosting wins, and the fact that tokens — not documents — are what the invoice line items count.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model versioning.&lt;/strong&gt; When OpenAI ships &lt;code&gt;text-embedding-4-tiny&lt;/code&gt; in 2027, how do you migrate a 10M-doc index without downtime? The right answer stamps every row with a &lt;code&gt;model_tag&lt;/code&gt;, dual-writes during migration, and keeps the ability to roll back. The wrong answer discovers there is no way to tell which vectors are on which model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drift monitoring.&lt;/strong&gt; What tells you the retrieval quality is degrading &lt;em&gt;before&lt;/em&gt; the product team files a bug? The senior answer is a golden query set + recall@k trend, not "we'll notice." Drift is silent; the pipeline that does not monitor for it is the pipeline that ships a regression.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why re-embed is not a "one-off migration."&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Corpora move.&lt;/strong&gt; Product catalogues update daily. Documentation is edited weekly. Support tickets stream in continuously. Any corpus that backs a semantic search is &lt;em&gt;always&lt;/em&gt; in motion; the pipeline is a stream, not a batch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Models move.&lt;/strong&gt; Between 2022 and 2026, OpenAI shipped &lt;code&gt;text-embedding-ada-002&lt;/code&gt;, then &lt;code&gt;text-embedding-3-small&lt;/code&gt;, then &lt;code&gt;text-embedding-3-large&lt;/code&gt; — each with a different dimension, a different price, and a different quality profile. Cohere shipped &lt;code&gt;embed-english-v3&lt;/code&gt;, then &lt;code&gt;embed-multilingual-v3&lt;/code&gt;, then &lt;code&gt;embed-v4&lt;/code&gt;. Self-hosted encoders (BGE, E5, GTE) release a new checkpoint every quarter. The stack the team picked at launch is not the stack in production two years later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vocabulary moves.&lt;/strong&gt; New product names, new terminology, new user intents show up in the query stream before they show up in the corpus. The embedding model's implicit vocabulary was frozen at training time; the corpus and the query stream keep moving. That gap is drift.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance moves.&lt;/strong&gt; GDPR erasure requests, DMCA takedowns, customer opt-outs — every one of these deletes a row from the corpus, and every one of those deletions must remove the matching vector from the store. Skip this and the pipeline ships a privacy bug.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What a healthy embeddings pipeline looks like end-to-end.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ingest layer.&lt;/strong&gt; New / updated / deleted documents flow in via CDC, Kafka, or an ETL job. Each document has a stable &lt;code&gt;doc_id&lt;/code&gt;, a content field, and a &lt;code&gt;sha256_hash&lt;/code&gt; of the content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Change-detection layer.&lt;/strong&gt; Compare the new hash to the last-seen hash; if different, mark the row for re-embed. Cheapest possible first pass — bytes in, bytes out, no model calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embed layer.&lt;/strong&gt; Batch the change queue, call the embedding API (or self-hosted encoder), write the resulting vector plus the &lt;code&gt;model_tag&lt;/code&gt; back to the row. This is the expensive layer; every cent spent here is a cent that comes off &lt;code&gt;embedding cost&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vector-store sync.&lt;/strong&gt; Upsert the vector into pgvector / Pinecone / Weaviate. Delete vectors for tombstoned rows. Track write success per batch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drift monitor.&lt;/strong&gt; Nightly job runs a golden query set through the retrieval pipeline; reports &lt;a href="mailto:recall@k"&gt;recall@k&lt;/a&gt;. Alert when the trend crosses the threshold.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model-migration path.&lt;/strong&gt; When the team decides to move from &lt;code&gt;text-embedding-3-small&lt;/code&gt; to a new model, a background job re-embeds the corpus into a shadow column, validates recall@k against the old, cuts the read path over, drops the old column.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What interviewers listen for.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do you say &lt;strong&gt;"content hash + model tag"&lt;/strong&gt; in the first sentence when asked how to detect a stale vector? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you mention &lt;strong&gt;batch-tier pricing (50% off)&lt;/strong&gt; unprompted when asked about cost? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you push back on &lt;strong&gt;"we re-embed everything nightly"&lt;/strong&gt; with the cost + drift argument? — required answer.&lt;/li&gt;
&lt;li&gt;Do you describe embedding drift as a &lt;strong&gt;trend on recall@k over a golden query set&lt;/strong&gt;, not as an intuition? — required answer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Worked example — the "we re-embed everything nightly" anti-pattern
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The textbook mistake: a team ships RAG on top of a 5-million-document knowledge base. On launch day they set up a nightly cron that iterates every row, calls &lt;code&gt;text-embedding-3-small&lt;/code&gt;, and upserts the vector. Everything works. Three months later finance flags a 15,000-dollar-a-month line item on the OpenAI invoice. Walk an interviewer through the true cost, the actual change rate, and the incremental architecture that replaces the nightly full re-embed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The symptom.&lt;/strong&gt; OpenAI invoice grows linearly with corpus size × refresh rate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The naive assumption.&lt;/strong&gt; "The API is cheap, so nightly is fine."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The real math.&lt;/strong&gt; 5M docs × 30 nights × average tokens per doc × per-token rate = a five-figure monthly bill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The correct fix.&lt;/strong&gt; Only re-embed rows whose content hash changed since the last run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; A 5-million-document corpus of internal wiki pages runs a nightly full-corpus re-embed on &lt;code&gt;text-embedding-3-small&lt;/code&gt; at $0.00002 per 1K tokens. Average doc length is 400 tokens. Actual per-day churn is 2% of rows. Quantify the wasted spend and propose the incremental architecture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Corpus size&lt;/td&gt;
&lt;td&gt;5,000,000 docs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Average tokens per doc&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model&lt;/td&gt;
&lt;td&gt;text-embedding-3-small&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Price per 1K tokens&lt;/td&gt;
&lt;td&gt;$0.00002&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refresh cadence&lt;/td&gt;
&lt;td&gt;nightly (30 runs / month)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Actual per-day change rate&lt;/td&gt;
&lt;td&gt;2%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Cost of the nightly full re-embed
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;full_reembed_monthly_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tokens_per_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price_per_1k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;runs_per_month&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;tokens_per_run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;docs&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;tokens_per_doc&lt;/span&gt;
    &lt;span class="n"&gt;price_per_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;price_per_1k&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;1000.0&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;docs&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;tokens_per_doc&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;runs_per_month&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;price_per_token&lt;/span&gt;

&lt;span class="c1"&gt;# Cost of the incremental re-embed
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;incremental_monthly_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tokens_per_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price_per_1k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;runs_per_month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;change_rate&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;full_reembed_monthly_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tokens_per_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price_per_1k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;runs_per_month&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;change_rate&lt;/span&gt;

&lt;span class="n"&gt;full_cost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;full_reembed_monthly_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5_000_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tokens_per_doc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;price_per_1k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.00002&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;runs_per_month&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;inc_cost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;incremental_monthly_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5_000_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tokens_per_doc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;price_per_1k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.00002&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;runs_per_month&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;change_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.02&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Full nightly:  $&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;full_cost&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;,.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/month&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Incremental:   $&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;inc_cost&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;,.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/month&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Savings:       $&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;full_cost&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;inc_cost&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;,.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/month  (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;inc_cost&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;full_cost&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;%)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Incremental change-queue schema (Postgres)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;doc_id&lt;/span&gt;        &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;content&lt;/span&gt;       &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;sha256_hash&lt;/span&gt;   &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;embedding&lt;/span&gt;     &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1536&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;model_tag&lt;/span&gt;     &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt;    &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;embedded_at&lt;/span&gt;   &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Change-queue is a view over docs whose content hash advanced past the embedded snapshot&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;embed_queue&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sha256_hash&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;documents&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="k"&gt;OR&lt;/span&gt;  &lt;span class="n"&gt;embedded_at&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="k"&gt;OR&lt;/span&gt;  &lt;span class="n"&gt;embedded_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The full nightly job re-embeds 5M docs × 400 tokens × 30 nights = 60 billion tokens per month. At $0.00002 per 1K tokens that is $1,200 per month straight to OpenAI — before the team even touched the model tier.&lt;/li&gt;
&lt;li&gt;The actual change rate is 2% per day, so at most 100K rows genuinely need re-embedding on any given night. 100K × 400 × 30 = 1.2 billion tokens per month — a 50× reduction on volume for the same freshness guarantee.&lt;/li&gt;
&lt;li&gt;The incremental architecture stores &lt;code&gt;sha256_hash&lt;/code&gt; on every row and updates &lt;code&gt;updated_at&lt;/code&gt; whenever content changes. The embed job selects &lt;code&gt;WHERE embedded_at IS NULL OR embedded_at &amp;lt; updated_at&lt;/code&gt; — only the rows that actually need work.&lt;/li&gt;
&lt;li&gt;The schema also stores &lt;code&gt;embedded_at&lt;/code&gt; — the timestamp of the last successful embed. This is the freshness cursor; a nightly job comparing &lt;code&gt;updated_at &amp;gt; embedded_at&lt;/code&gt; finds exactly the rows that drifted since last run.&lt;/li&gt;
&lt;li&gt;The nightly cron becomes: (a) select from &lt;code&gt;embed_queue&lt;/code&gt;, (b) batch, (c) call the API, (d) &lt;code&gt;UPDATE documents SET embedding = ..., embedded_at = now(), model_tag = ...&lt;/code&gt; for each returned vector. The remaining 98% of rows are never touched.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Architecture&lt;/th&gt;
&lt;th&gt;Docs re-embedded / night&lt;/th&gt;
&lt;th&gt;Tokens / month&lt;/th&gt;
&lt;th&gt;Monthly cost&lt;/th&gt;
&lt;th&gt;Savings&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Full nightly re-embed&lt;/td&gt;
&lt;td&gt;5,000,000&lt;/td&gt;
&lt;td&gt;60,000,000,000&lt;/td&gt;
&lt;td&gt;$1,200&lt;/td&gt;
&lt;td&gt;baseline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Incremental (2% churn)&lt;/td&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;td&gt;1,200,000,000&lt;/td&gt;
&lt;td&gt;$24&lt;/td&gt;
&lt;td&gt;98%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Incremental + batch tier&lt;/td&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;td&gt;1,200,000,000&lt;/td&gt;
&lt;td&gt;$12&lt;/td&gt;
&lt;td&gt;99%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Never re-embed a corpus you have not verified is dirty. The content-hash + &lt;code&gt;embedded_at&lt;/code&gt; cursor turns a linear-in-corpus job into a linear-in-churn job. The cost delta is usually 20× to 100×; the freshness guarantee is identical.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — mapping the four axes to a job flow
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Senior interviewers often ask the candidate to sketch the pipeline diagram from first principles. The correct answer is a four-layer flow — ingest, change detection, embed, sync — with a drift monitor as a fifth sidecar. Every arrow in the flow is one of the four axes the interviewer is probing. Walk through the flow and name which axis each arrow addresses.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The ingest arrow&lt;/strong&gt; addresses the &lt;em&gt;incremental&lt;/em&gt; axis — CDC or ETL brings only changed rows into scope.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The embed arrow&lt;/strong&gt; addresses the &lt;em&gt;cost&lt;/em&gt; axis — batch, tier, model choice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The write-back arrow&lt;/strong&gt; addresses the &lt;em&gt;versioning&lt;/em&gt; axis — the &lt;code&gt;model_tag&lt;/code&gt; column tags every vector.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The drift-monitor sidecar&lt;/strong&gt; addresses the &lt;em&gt;drift&lt;/em&gt; axis — golden queries + &lt;a href="mailto:recall@k"&gt;recall@k&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Draw the end-to-end embeddings pipeline for a knowledge-base RAG stack and annotate each edge with the axis it addresses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;th&gt;Axis addressed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Source (Postgres knowledge_base)&lt;/td&gt;
&lt;td&gt;System of record&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CDC (Debezium)&lt;/td&gt;
&lt;td&gt;Change stream&lt;/td&gt;
&lt;td&gt;Incremental&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change queue&lt;/td&gt;
&lt;td&gt;SHA-256 hash diff, embed_queue view&lt;/td&gt;
&lt;td&gt;Incremental&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Embed batch job&lt;/td&gt;
&lt;td&gt;Call embedding API in batches&lt;/td&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vector store (pgvector)&lt;/td&gt;
&lt;td&gt;Serve retrieval&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drift monitor&lt;/td&gt;
&lt;td&gt;Nightly golden-query recall@k&lt;/td&gt;
&lt;td&gt;Drift&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model migration job&lt;/td&gt;
&lt;td&gt;Shadow column re-embed&lt;/td&gt;
&lt;td&gt;Versioning&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# End-to-end pipeline sketch — Prefect / Dagster / Airflow flavour agnostic
&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Doc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;sha256_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ingest&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;CDC or ETL yields only rows that changed since last run.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;cdc_stream&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nc"&gt;Doc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;doc_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change_detect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Compare hash to last-seen; only forward changed rows.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;last_hash_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sha256_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;embed_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-embedding-3-small&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Batch call the embedding API for cost efficiency.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="nf"&gt;embed_and_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="nf"&gt;embed_and_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;write_back&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Upsert vector + model_tag into pgvector.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="nf"&gt;upsert_pgvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;drift_monitor&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Sidecar: nightly recall@k against a golden query set.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;recall_at_k&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;golden_queries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;emit_metric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;embedding.recall_at_10&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;page_oncall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;embedding drift alert&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Drive
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;refresh_pipeline&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;changed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;change_detect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;ingest&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="nf"&gt;write_back&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;embed_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;changed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-embedding-3-small&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;ingest()&lt;/code&gt; reads only rows that changed since the last CDC checkpoint. This is the &lt;em&gt;incremental&lt;/em&gt; boundary — the pipeline never sees the 98% of rows that did not change. Full-corpus scans are a cost bug; CDC is the cure.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;change_detect()&lt;/code&gt; guards against the case where an upstream job touched &lt;code&gt;updated_at&lt;/code&gt; without actually changing the content (a common Postgres UPSERT footgun). Hash the content bytes; forward only genuine mismatches.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;embed_batch()&lt;/code&gt; accumulates 256 rows before calling the API. Providers charge per token, but network overhead per request is fixed; batching 256 rows into one HTTP call is 256× the throughput of one-row-per-call. Cost per document falls proportionally.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;write_back()&lt;/code&gt; upserts the vector and the &lt;code&gt;model_tag&lt;/code&gt; in the same transaction. Every vector is stamped with the model that produced it; migrations later query on this tag to find "old-model" rows.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;drift_monitor()&lt;/code&gt; is a sidecar — it runs on a separate schedule, not in the write path. Its output is a metric, not a gate. The pipeline never blocks on drift; the pager fires if the trend crosses a threshold.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Edge&lt;/th&gt;
&lt;th&gt;From&lt;/th&gt;
&lt;th&gt;To&lt;/th&gt;
&lt;th&gt;Axis&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ingest → change-detect&lt;/td&gt;
&lt;td&gt;Source&lt;/td&gt;
&lt;td&gt;Hash diff&lt;/td&gt;
&lt;td&gt;Incremental&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;change-detect → embed&lt;/td&gt;
&lt;td&gt;Queue&lt;/td&gt;
&lt;td&gt;API batch&lt;/td&gt;
&lt;td&gt;Incremental + cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;embed → write-back&lt;/td&gt;
&lt;td&gt;API&lt;/td&gt;
&lt;td&gt;pgvector&lt;/td&gt;
&lt;td&gt;Cost + versioning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;write-back → serve&lt;/td&gt;
&lt;td&gt;pgvector&lt;/td&gt;
&lt;td&gt;Retrieval&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;corpus → drift-monitor&lt;/td&gt;
&lt;td&gt;Serve path&lt;/td&gt;
&lt;td&gt;Metric&lt;/td&gt;
&lt;td&gt;Drift&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Any embeddings pipeline missing any of the four arrows is a pipeline waiting for an incident. Draw the diagram before you write the first line of code; label the axes; check them off in every design review.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — GDPR erasure request through the pipeline
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A user submits a GDPR erasure request. Their record in Postgres is deleted. If the vector store still has their embedding, retrieval can leak content the user has legally requested erased. The pipeline must propagate the delete end-to-end. Walk through the tombstone pattern that makes deletes as first-class as inserts.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The compliance requirement.&lt;/strong&gt; GDPR Article 17 mandates deletion "without undue delay." Practical SLA is 24 hours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The naive bug.&lt;/strong&gt; The nightly re-embed skips deleted rows and never issues a delete on the vector store. The vector lives on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The fix.&lt;/strong&gt; A tombstone row (or a &lt;code&gt;deleted_at&lt;/code&gt; column) drives an explicit delete on the vector store.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Extend the pipeline so a Postgres row deletion results in a Pinecone / pgvector delete within 15 minutes. Show the schema, the delta stream, and the vector-store call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Corpus size&lt;/td&gt;
&lt;td&gt;5M docs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delete rate&lt;/td&gt;
&lt;td&gt;~100/day (steady state), spikes on legal takedown days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compliance SLA&lt;/td&gt;
&lt;td&gt;24 hours (aim: 15 minutes)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vector store&lt;/td&gt;
&lt;td&gt;pgvector&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Soft-delete pattern: tombstone rather than DELETE&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;deleted_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;embed_delete_queue&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;doc_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;documents&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;deleted_at&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt;  &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;-- still has a vector to remove&lt;/span&gt;

&lt;span class="c1"&gt;-- After the sync job runs, wipe the vector column and mark the row purged&lt;/span&gt;
&lt;span class="c1"&gt;-- (deleted_at is kept for audit; embedding is NULL to mark tombstone processed)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Deletion sync — small, frequent job
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sync_deletes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgres://svc@db.internal/analytics&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;autocommit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        SELECT doc_id
        FROM   embed_delete_queue
        LIMIT  %s
        FOR UPDATE SKIP LOCKED
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;
    &lt;span class="n"&gt;to_purge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;to_purge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rollback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="c1"&gt;# Vector-store delete (pgvector: just NULL the column and DELETE from mirrors)
&lt;/span&gt;    &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        UPDATE documents
        SET    embedding  = NULL,
               model_tag  = NULL
        WHERE  doc_id = ANY(%s)
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to_purge&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to_purge&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The system-of-record row is never hard-deleted immediately. Instead, the app writes &lt;code&gt;deleted_at = now()&lt;/code&gt; — a soft-delete tombstone. This preserves audit lineage while flagging the row for downstream propagation.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;embed_delete_queue&lt;/code&gt; view isolates rows that are tombstoned &lt;em&gt;and&lt;/em&gt; still carry a vector. Once the delete job runs and NULLs the vector, the row drops out of the view — the queue self-drains.&lt;/li&gt;
&lt;li&gt;The sync job runs every minute (or on CDC event). It selects a batch with &lt;code&gt;FOR UPDATE SKIP LOCKED&lt;/code&gt; — safe under concurrency, so multiple worker instances can run without stomping on the same row.&lt;/li&gt;
&lt;li&gt;The vector-store call is a single &lt;code&gt;UPDATE documents SET embedding = NULL&lt;/code&gt; in pgvector. For Pinecone, it would be &lt;code&gt;index.delete(ids=to_purge)&lt;/code&gt;. For Weaviate, a batch DELETE by ID.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;deleted_at&lt;/code&gt; column stays populated forever — it is the audit trail. &lt;code&gt;embedding = NULL&lt;/code&gt; is the "processed" flag. If a future engineer needs to prove GDPR compliance, the query is &lt;code&gt;SELECT doc_id, deleted_at FROM documents WHERE embedding IS NULL AND deleted_at IS NOT NULL&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;SLA&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;User erasure request&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UPDATE documents SET deleted_at = now()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;embed_delete_queue drains&lt;/td&gt;
&lt;td&gt;Vector NULL'd, mirrors purged&lt;/td&gt;
&lt;td&gt;1–15 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retrieval no longer returns the vector&lt;/td&gt;
&lt;td&gt;pgvector query skips NULL vectors&lt;/td&gt;
&lt;td&gt;immediate after sync&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audit trail&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;deleted_at&lt;/code&gt; column retained&lt;/td&gt;
&lt;td&gt;forever&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Model deletes as first-class events, not as "the absence of an insert." The tombstone pattern makes compliance auditable and lets the same pipeline handle inserts, updates, and deletes with the same change-queue mechanism.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on the four axes of embedding refresh
&lt;/h3&gt;

&lt;p&gt;A senior interviewer often opens with: "You inherit a RAG stack that re-embeds the entire 10-million-document corpus every night on &lt;code&gt;text-embedding-3-small&lt;/code&gt;. The CFO wants the OpenAI bill cut 90%. The PM wants freshness under 15 minutes. Walk me through the architecture you ship, what you measure, and how you'd sequence the migration."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a content-hash change queue, batch pricing tier, and CDC-driven incremental refresh
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Blueprint — the four-axis embeddings refresh pipeline
&lt;/span&gt;
&lt;span class="c1"&gt;# 1. INGEST (incremental)  — CDC stream from Postgres via Debezium
# 2. CHANGE DETECT         — SHA-256 hash + embedded_at cursor
# 3. EMBED BATCH           — text-embedding-3-small, batch=256, batch-tier pricing
# 4. WRITE-BACK            — pgvector upsert + model_tag column
# 5. DRIFT MONITOR         — nightly recall@10 on golden queries; alert &amp;lt; 0.85
&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Iterable&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;psycopg2.extras&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;execute_values&lt;/span&gt;

&lt;span class="n"&gt;MODEL&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-embedding-3-small&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;DIMS&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1536&lt;/span&gt;
&lt;span class="n"&gt;BATCH&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sha256_of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;stream_changed_rows&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Iterable&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Debezium → Kafka → this consumer. Yields only rows whose content changed.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;kafka_consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cdc.documents&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;
        &lt;span class="n"&gt;new_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sha256_of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;new_hash&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sha256_hash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sha256_hash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_hash&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;embed_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;texts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;texts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;embedding&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;
        &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model_tag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;MODEL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;@v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;upsert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;doc_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sha256_hash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;embedding&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model_tag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nf"&gt;execute_values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        INSERT INTO documents (doc_id, sha256_hash, embedding, model_tag, embedded_at)
        VALUES %s
        ON CONFLICT (doc_id) DO UPDATE
          SET sha256_hash = EXCLUDED.sha256_hash,
              embedding   = EXCLUDED.embedding,
              model_tag   = EXCLUDED.model_tag,
              embedded_at = now()
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(%s, %s, %s, %s, now())&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgres://svc@db.internal/rag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;stream_changed_rows&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;BATCH&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;upsert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;embed_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;upsert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;embed_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Before (nightly full)&lt;/th&gt;
&lt;th&gt;After (CDC + hash)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Docs embedded / day&lt;/td&gt;
&lt;td&gt;10,000,000&lt;/td&gt;
&lt;td&gt;200,000 (2% churn)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tokens / month&lt;/td&gt;
&lt;td&gt;120 billion&lt;/td&gt;
&lt;td&gt;2.4 billion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monthly cost @ $0.00002/1K&lt;/td&gt;
&lt;td&gt;$2,400&lt;/td&gt;
&lt;td&gt;$48&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch-tier savings (50%)&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;$24&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Freshness (avg age of embedding)&lt;/td&gt;
&lt;td&gt;12 hours&lt;/td&gt;
&lt;td&gt;&amp;lt; 15 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model-tag column on every row&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drift monitor&lt;/td&gt;
&lt;td&gt;absent&lt;/td&gt;
&lt;td&gt;nightly recall@10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After the rollout, the OpenAI invoice drops from $2,400 to under $50 per month, freshness improves from a 12-hour average to sub-15-minute p99, and every row carries its own &lt;code&gt;model_tag&lt;/code&gt; so the next model migration is a controlled shadow-column rollout instead of a full-corpus outage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Docs re-embedded / day&lt;/td&gt;
&lt;td&gt;10M&lt;/td&gt;
&lt;td&gt;200K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monthly cost&lt;/td&gt;
&lt;td&gt;$2,400&lt;/td&gt;
&lt;td&gt;$24 (batch tier)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Embedding freshness p99&lt;/td&gt;
&lt;td&gt;12 h&lt;/td&gt;
&lt;td&gt;&amp;lt; 15 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model tag on every row&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drift alerting&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;recall@10 nightly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Content hash as the primary lever&lt;/strong&gt;&lt;/strong&gt; — the SHA-256 diff means the pipeline pays only for the rows that changed. On a corpus with 2% daily churn, this is a 50× reduction in tokens and dollars.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;CDC over polling&lt;/strong&gt;&lt;/strong&gt; — Debezium (or Postgres logical decoding) delivers changes with sub-second latency and zero query load on the source. Polling every 5 minutes with &lt;code&gt;WHERE updated_at &amp;gt; $cursor&lt;/code&gt; works but adds warehouse load; CDC is the right pattern past 1M rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Batch pricing tier&lt;/strong&gt;&lt;/strong&gt; — OpenAI and Cohere both offer a batch-inference tier at roughly 50% of real-time pricing. For refresh workloads (as opposed to inference workloads), the 24-hour turnaround is a non-issue. Free 50% by moving refresh onto the batch tier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Model_tag column&lt;/strong&gt;&lt;/strong&gt; — every vector is stamped with the exact model + version that produced it. Migration to a new model is now a "re-embed rows where &lt;code&gt;model_tag != current_model&lt;/code&gt;" job; rollback is an &lt;code&gt;UPDATE documents SET embedding = old_embedding WHERE model_tag = 'ada-002@v1'&lt;/code&gt; if the shadow column was kept.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — the pipeline's runtime cost is O(changed_rows) per day, not O(corpus_size). On a 10M-doc corpus with 2% churn that is a 50× cost reduction with no loss of freshness. The drift-monitor sidecar adds negligible cost (one golden-query call per night); the compliance win of the tombstone pattern is priceless.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — sql&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL problems on change-detection queries and hash diffs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;ETL&lt;/span&gt;
&lt;span&gt;Topic — etl&lt;/span&gt;
&lt;strong&gt;ETL problems on incremental refresh pipelines&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  2. Change detection + incremental refresh
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Content hash + &lt;code&gt;embedded_at&lt;/code&gt; cursor is the whole incremental story — everything else is a variation
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;an &lt;code&gt;incremental embedding&lt;/code&gt; job is a &lt;code&gt;WHERE embedded_at &amp;lt; updated_at&lt;/code&gt; query with a SHA-256 content hash to guard against false positives, and everything more elaborate — CDC streams, Kafka fanouts, materialised change queues — is an optimisation of the same primitive&lt;/strong&gt;. Get the primitive right and the rest of the pipeline follows; get it wrong and you either miss updates or re-embed the whole corpus.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3ljwurlv7d2if3fw25gu.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3ljwurlv7d2if3fw25gu.jpeg" alt="Iconographic change detection diagram — a document table with a sha256_hash column, a diff-glyph comparing old vs new hash, and a re-embed queue on the right, with SHA-256, changed, and re-embed labels, on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four "must-answer" axes for change detection.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Change signal.&lt;/strong&gt; What tells you a document changed? Options: content hash, &lt;code&gt;updated_at&lt;/code&gt; timestamp, CDC event, ETL delta table. Each has a false-positive / false-negative profile. Hash is the safest; timestamp is the cheapest; CDC is the freshest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Change queue.&lt;/strong&gt; Where do you materialise the "these rows need re-embedding" list? Options: a view (&lt;code&gt;embed_queue&lt;/code&gt;), a Kafka topic, a Redis stream, a Postgres queue table. Materialisation is the operational lever — a view is the simplest, a Kafka topic is the highest-throughput.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch cadence.&lt;/strong&gt; How often do you drain the queue? Options: minute-level for fresh corpora, hourly for slow-moving, nightly for stable. Cadence is the freshness knob — tune it against the SLA and the cost tier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotency + retry.&lt;/strong&gt; What happens if the embed call fails halfway through the batch? The queue must survive a crash, and re-runs must not double-embed. &lt;code&gt;FOR UPDATE SKIP LOCKED&lt;/code&gt; on the queue rows is the standard Postgres pattern.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Change signal — the four sources.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Content hash.&lt;/strong&gt; Cheapest first pass. &lt;code&gt;sha256_hash = sha256(content)&lt;/code&gt; computed at write time or by a CDC transformer. Compare the new hash to the last-known hash; if different, flag for re-embed. Immune to spurious &lt;code&gt;updated_at&lt;/code&gt; bumps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;updated_at&lt;/code&gt; timestamp.&lt;/strong&gt; Every OLTP schema already has this. Query &lt;code&gt;WHERE embedded_at &amp;lt; updated_at&lt;/code&gt; — trivial. Downside: an &lt;code&gt;UPDATE documents SET updated_at = now() WHERE ...&lt;/code&gt; that touched no content still flags the row.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CDC event.&lt;/strong&gt; Debezium / logical decoding streams every row change with sub-second latency. Highest freshness; requires infrastructure (Kafka + Debezium + a consumer).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ETL delta table.&lt;/strong&gt; For batch-heavy stacks (dbt / Airflow), a nightly &lt;code&gt;changed_docs&lt;/code&gt; table is idiomatic. Simple, but freshness is bound by the batch cadence.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Change-queue materialisation — four options.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;View (&lt;code&gt;embed_queue&lt;/code&gt;).&lt;/strong&gt; &lt;code&gt;CREATE VIEW ... AS SELECT ... WHERE embedded_at &amp;lt; updated_at&lt;/code&gt;. Zero storage; the embed worker just queries the view. Best for small-to-medium corpora with &amp;lt; 1M change events per day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres queue table.&lt;/strong&gt; &lt;code&gt;INSERT INTO embed_queue (doc_id) ...&lt;/code&gt; on write; worker &lt;code&gt;DELETE ... RETURNING ...&lt;/code&gt;. Durable, transactional, easy to reason about. The standard pattern for team-managed pipelines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kafka topic.&lt;/strong&gt; CDC → Kafka → embed consumer. Highest throughput; scales to millions of events per hour. Adds operational overhead (Kafka + schema registry + consumer offsets).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis stream / Sidekiq / SQS.&lt;/strong&gt; For non-Postgres stacks. Same semantics as the Kafka option, different infrastructure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cadence — matching the tier to the SLA.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Minute-level.&lt;/strong&gt; Product catalogues (price / stock changes), news feeds, user-generated content. The freshness SLA is single-digit minutes; the pipeline runs continuously or every 60 seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hourly.&lt;/strong&gt; Documentation, wiki content, internal knowledge bases. 1-hour p99 freshness is fine; hourly cron jobs are the simplest fit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nightly.&lt;/strong&gt; Reference data (product taxonomies, employee directories, static help centre articles). Once a day is plenty; run overnight to align with the batch-tier pricing window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On-demand.&lt;/strong&gt; Manual refresh triggered by an editor's "publish" button, dry-run in a staging environment, or a compliance takedown that needs immediate propagation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Idempotency — the boring but critical part.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;FOR UPDATE SKIP LOCKED&lt;/code&gt;.&lt;/strong&gt; Standard Postgres pattern for a queue table. Multiple workers can drain the queue in parallel without stepping on each other's toes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotency key.&lt;/strong&gt; Each embed call is keyed by &lt;code&gt;(doc_id, sha256_hash)&lt;/code&gt;. A retry re-computes the same vector; the upsert is a no-op if the vector already exists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Advisory locks for whole-corpus operations.&lt;/strong&gt; A model migration that touches every row should acquire &lt;code&gt;pg_advisory_xact_lock(hashtext('embed_migration_v3'))&lt;/code&gt; — prevents two migration jobs racing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Poison-pill handling.&lt;/strong&gt; A document whose content triggers a provider error (bad UTF-8, oversized, blocked by content policy) should not permanently jam the queue. Track &lt;code&gt;embed_attempts&lt;/code&gt; and route rows past 3 failures to a dead-letter table.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on change detection.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Why hash the content instead of just trusting &lt;code&gt;updated_at&lt;/code&gt;?" — spurious touches, ETL writes that bump &lt;code&gt;updated_at&lt;/code&gt; without content change.&lt;/li&gt;
&lt;li&gt;"How does your queue survive a worker crash mid-batch?" — &lt;code&gt;FOR UPDATE SKIP LOCKED&lt;/code&gt; + transactional consume + retry-safe upsert.&lt;/li&gt;
&lt;li&gt;"When do you switch from a view to a Kafka topic?" — throughput; roughly &amp;gt; 10K change events per hour on the Postgres side.&lt;/li&gt;
&lt;li&gt;"How do you handle deletes?" — tombstone pattern with a &lt;code&gt;deleted_at&lt;/code&gt; column; the vector-store delete is a separate sync.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — SHA-256 hash column and the &lt;code&gt;embed_queue&lt;/code&gt; view
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The simplest possible incremental refresh: add a &lt;code&gt;sha256_hash&lt;/code&gt; column to the source table, compute it in Postgres or the ETL, and select the rows whose hash advanced past the embedded snapshot. The view + worker pattern handles corpora up to about 1M change events per day without any additional infrastructure.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The schema.&lt;/strong&gt; &lt;code&gt;documents(doc_id PK, content, sha256_hash, embedding, model_tag, updated_at, embedded_at)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The view.&lt;/strong&gt; &lt;code&gt;embed_queue AS SELECT ... WHERE embedded_at IS NULL OR embedded_at &amp;lt; updated_at&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The worker.&lt;/strong&gt; &lt;code&gt;SELECT ... FROM embed_queue FOR UPDATE SKIP LOCKED LIMIT 256&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Design the schema, the view, the worker, and the retry-safe upsert for an incremental refresh on a 500K-doc corpus with 5% daily churn.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Corpus&lt;/td&gt;
&lt;td&gt;500K docs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Daily churn&lt;/td&gt;
&lt;td&gt;5% (25K rows)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change signal&lt;/td&gt;
&lt;td&gt;content hash + &lt;code&gt;updated_at&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Freshness SLA&lt;/td&gt;
&lt;td&gt;15 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worker cadence&lt;/td&gt;
&lt;td&gt;every 60 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Schema&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;pgcrypto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;doc_id&lt;/span&gt;        &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;content&lt;/span&gt;       &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;sha256_hash&lt;/span&gt;   &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;embedding&lt;/span&gt;     &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1536&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;model_tag&lt;/span&gt;     &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt;    &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;embedded_at&lt;/span&gt;   &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;embed_attempts&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;documents_embed_queue&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;embedded_at&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;embedded_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Trigger keeps sha256_hash in sync with content&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;doc_hash_trigger&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;OLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;OLD&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
    &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sha256_hash&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'sha256'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'hex'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt;  &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt; &lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="n"&gt;trg_doc_hash&lt;/span&gt;
  &lt;span class="k"&gt;BEFORE&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;
  &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;EACH&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;doc_hash_trigger&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;-- Queue view&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;embed_queue&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sha256_hash&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;documents&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedded_at&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;embedded_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt;  &lt;span class="n"&gt;embed_attempts&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Worker — every 60 s
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;psycopg2.extras&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;execute_batch&lt;/span&gt;

&lt;span class="n"&gt;BATCH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;
&lt;span class="n"&gt;MODEL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-embedding-3-small&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        SELECT doc_id, content, sha256_hash
        FROM   documents
        WHERE  embedded_at IS NULL OR embedded_at &amp;lt; updated_at
        ORDER  BY updated_at
        LIMIT  %s
        FOR UPDATE SKIP LOCKED
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BATCH&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rollback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="n"&gt;texts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;texts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;updates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;MODEL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;@v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nf"&gt;execute_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        UPDATE documents
        SET    embedding    = %s,
               model_tag    = %s,
               embedded_at  = now(),
               embed_attempts = 0
        WHERE  doc_id = %s
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updates&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;documents&lt;/code&gt; table stores the content, a hash of the content, the vector, the model tag that produced the vector, and two timestamps: &lt;code&gt;updated_at&lt;/code&gt; (bumped on content change) and &lt;code&gt;embedded_at&lt;/code&gt; (bumped after a successful embed).&lt;/li&gt;
&lt;li&gt;The BEFORE INSERT / UPDATE trigger computes &lt;code&gt;sha256_hash&lt;/code&gt; in Postgres itself. This guards against ETL writes that forget to compute the hash and against a bug in the application layer — the source of truth for "the content changed" is a Postgres invariant.&lt;/li&gt;
&lt;li&gt;The partial index on &lt;code&gt;(updated_at) WHERE embedded_at IS NULL OR embedded_at &amp;lt; updated_at&lt;/code&gt; makes the queue query O(queue_depth), not O(corpus). On a 500K-doc corpus with a 25K/day queue, the index is 25K entries — fits in memory, scans in milliseconds.&lt;/li&gt;
&lt;li&gt;The worker runs every 60 seconds, selects up to 256 rows with &lt;code&gt;FOR UPDATE SKIP LOCKED&lt;/code&gt;, calls the embedding API, and updates the vector + &lt;code&gt;embedded_at&lt;/code&gt; in one transaction. If the API call fails, the transaction rolls back and the rows return to the queue.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;embed_attempts &amp;lt; 3&lt;/code&gt; in the view guards against poison pills — a document that persistently fails is dropped from the queue after 3 attempts and a nightly job routes it to a dead-letter table for manual triage.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cadence&lt;/th&gt;
&lt;th&gt;Queue depth (avg)&lt;/th&gt;
&lt;th&gt;Freshness p99&lt;/th&gt;
&lt;th&gt;Cost / day&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Nightly full re-embed&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;24 h&lt;/td&gt;
&lt;td&gt;$12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hourly incremental&lt;/td&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;60 min&lt;/td&gt;
&lt;td&gt;$0.60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;60-second incremental&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;60 s&lt;/td&gt;
&lt;td&gt;$0.60&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For any corpus over ~50K rows, the hash column + queue view + partial index pattern is the right default. The trigger keeps the hash trustworthy; the partial index keeps the queue query fast; &lt;code&gt;FOR UPDATE SKIP LOCKED&lt;/code&gt; keeps the workers safe.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — CDC-triggered incremental refresh with Debezium + Kafka
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; For high-throughput corpora (millions of change events per day) or teams already running Kafka, CDC is the higher-leverage pattern. Debezium captures the Postgres write-ahead log, emits one Kafka message per row change, and a consumer batches messages, calls the embedding API, and upserts. Freshness is sub-second; throughput scales horizontally by consumer group size.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CDC source.&lt;/strong&gt; Debezium Postgres connector reads WAL via logical decoding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kafka topic.&lt;/strong&gt; &lt;code&gt;cdc.documents&lt;/code&gt; — one message per row change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consumer.&lt;/strong&gt; Kafka consumer group runs N workers; each drains a partition, batches 256, embeds, upserts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Wire Debezium → Kafka → embed consumer for a corpus with 500K change events per day. Show the Debezium config, the Kafka schema, and the consumer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Corpus size&lt;/td&gt;
&lt;td&gt;20M docs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change events / day&lt;/td&gt;
&lt;td&gt;500,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Freshness SLA&lt;/td&gt;
&lt;td&gt;30 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kafka partitions&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consumer group size&lt;/td&gt;
&lt;td&gt;4 workers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Debezium Postgres connector config&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pg-docs-connector&lt;/span&gt;
&lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;connector.class&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;io.debezium.connector.postgresql.PostgresConnector&lt;/span&gt;
  &lt;span class="na"&gt;database.hostname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;db.internal&lt;/span&gt;
  &lt;span class="na"&gt;database.port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5432&lt;/span&gt;
  &lt;span class="na"&gt;database.user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;debezium&lt;/span&gt;
  &lt;span class="na"&gt;database.dbname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rag&lt;/span&gt;
  &lt;span class="na"&gt;database.server.name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rag_pg&lt;/span&gt;
  &lt;span class="na"&gt;table.include.list&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;public.documents&lt;/span&gt;
  &lt;span class="na"&gt;plugin.name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pgoutput&lt;/span&gt;
  &lt;span class="na"&gt;publication.autocreate.mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;filtered&lt;/span&gt;
  &lt;span class="na"&gt;slot.name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;debezium_documents&lt;/span&gt;
  &lt;span class="c1"&gt;# Emit only the fields we need for embedding&lt;/span&gt;
  &lt;span class="na"&gt;transforms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unwrap&lt;/span&gt;
  &lt;span class="na"&gt;transforms.unwrap.type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;io.debezium.transforms.ExtractNewRecordState&lt;/span&gt;
  &lt;span class="na"&gt;transforms.unwrap.drop.tombstones&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Embed consumer — 1 worker per Kafka partition
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;kafka&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;KafkaConsumer&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;psycopg2.extras&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;execute_values&lt;/span&gt;

&lt;span class="n"&gt;MODEL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-embedding-3-small&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;BATCH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;consume&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;consumer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;KafkaConsumer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cdc.documents&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;bootstrap_servers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kafka:9092&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;group_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;embed-worker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;auto_offset_reset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;earliest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;enable_auto_commit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;max_poll_records&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;BATCH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgres://embed@db.internal/rag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;msgs&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;batches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;consumer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BATCH&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;msgs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__op&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="c1"&gt;# delete tombstone
&lt;/span&gt;                &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;doc_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;doc_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;op&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delete&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;new_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;new_hash&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sha256_hash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;                          &lt;span class="c1"&gt;# hash matches → skip
&lt;/span&gt;            &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;doc_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;doc_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sha256_hash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;new_hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;op&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;upsert&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;consumer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;upserts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;op&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;upsert&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;deletes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;doc_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;op&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delete&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;upserts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;upserts&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;doc_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sha256_hash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;MODEL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;@v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;upserts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nf"&gt;execute_values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            INSERT INTO documents (doc_id, sha256_hash, embedding, model_tag, embedded_at)
            VALUES %s
            ON CONFLICT (doc_id) DO UPDATE
              SET sha256_hash = EXCLUDED.sha256_hash,
                  embedding   = EXCLUDED.embedding,
                  model_tag   = EXCLUDED.model_tag,
                  embedded_at = now()
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(%s, %s, %s, %s, now())&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;deletes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UPDATE documents SET embedding = NULL, model_tag = NULL WHERE doc_id = ANY(%s)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deletes&lt;/span&gt;&lt;span class="p"&gt;,),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Debezium tails the Postgres WAL via logical decoding on the &lt;code&gt;debezium_documents&lt;/code&gt; replication slot. Every INSERT / UPDATE / DELETE against &lt;code&gt;public.documents&lt;/code&gt; becomes a message on the &lt;code&gt;cdc.documents&lt;/code&gt; Kafka topic. There is no polling; latency from row change to Kafka is sub-second.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;ExtractNewRecordState&lt;/code&gt; transform unwraps the Debezium envelope, leaving a flat JSON payload (&lt;code&gt;doc_id&lt;/code&gt;, &lt;code&gt;content&lt;/code&gt;, &lt;code&gt;sha256_hash&lt;/code&gt;, &lt;code&gt;__op&lt;/code&gt;) that the consumer can process directly.&lt;/li&gt;
&lt;li&gt;The consumer runs in a group of 4 workers across 8 partitions — 2 partitions per worker. Kafka's partition assignment guarantees that no two workers process the same doc_id concurrently, eliminating write-conflict handling.&lt;/li&gt;
&lt;li&gt;The consumer batches up to 256 messages, filters out no-op events (hash unchanged), calls the OpenAI embedding API once for the whole batch, and upserts via &lt;code&gt;execute_values&lt;/code&gt; with &lt;code&gt;ON CONFLICT DO UPDATE&lt;/code&gt;. &lt;code&gt;enable_auto_commit = False&lt;/code&gt; + explicit &lt;code&gt;consumer.commit()&lt;/code&gt; after the DB commit gives exactly-once semantics at the consumer boundary.&lt;/li&gt;
&lt;li&gt;Deletes are handled by the same consumer via the Debezium tombstone (&lt;code&gt;__op == 'd'&lt;/code&gt;). The vector is NULL'd; the tombstone in Postgres remains as an audit record.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Polling every 60 s&lt;/th&gt;
&lt;th&gt;Debezium CDC&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Freshness p99&lt;/td&gt;
&lt;td&gt;60 s&lt;/td&gt;
&lt;td&gt;&amp;lt; 2 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source-DB query load&lt;/td&gt;
&lt;td&gt;1440 queries/day&lt;/td&gt;
&lt;td&gt;0 (WAL tail)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Throughput ceiling&lt;/td&gt;
&lt;td&gt;limited by query&lt;/td&gt;
&lt;td&gt;horizontal (partition ↑)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operational cost&lt;/td&gt;
&lt;td&gt;trivial&lt;/td&gt;
&lt;td&gt;Kafka + Debezium infra&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Move to CDC when either freshness needs to be sub-minute or when the source-DB load from polling becomes non-trivial. Under those thresholds, the queue-view pattern is cheaper to run and easier to reason about.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — nightly batch refresh for a slow-moving corpus
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Not every corpus needs sub-second freshness. A legal knowledge base, a static help centre, a reference product taxonomy — these change once a week at most. For slow-moving corpora, a nightly batch job runs on the cheapest tier, aligns with the 24-hour batch pricing window, and eliminates the operational overhead of a streaming pipeline. Walk through the batch pattern.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The corpus.&lt;/strong&gt; 2M docs; change rate ~500 rows/week; freshness SLA 24 hours is fine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The tier.&lt;/strong&gt; OpenAI batch API — 50% cheaper than real-time inference.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The cadence.&lt;/strong&gt; One nightly job that submits the batch and polls for completion.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Ship a nightly batch job that finds the changed rows, submits them to the OpenAI batch API, and upserts the results the next morning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Corpus size&lt;/td&gt;
&lt;td&gt;2M docs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weekly churn&lt;/td&gt;
&lt;td&gt;500 rows (~70/day)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Freshness SLA&lt;/td&gt;
&lt;td&gt;24 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tier&lt;/td&gt;
&lt;td&gt;OpenAI batch API (50% off)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Nightly batch job — submitted at 22:00; results polled at 06:00 next day
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;

&lt;span class="n"&gt;MODEL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-embedding-3-small&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_batch_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Write a JSONL file with one request per changed row.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        SELECT doc_id, content, sha256_hash
        FROM   documents
        WHERE  embedded_at IS NULL OR embedded_at &amp;lt; updated_at
        ORDER  BY doc_id
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NamedTemporaryFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;suffix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delete&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sha&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;custom_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;::&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sha&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/v1/embeddings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;submit_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;upload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;purpose&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;batch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;batch&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;batches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;input_file_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;upload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/v1/embeddings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;completion_window&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;24h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;poll_and_apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;batches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output_file_id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;rec&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;custom_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;custom_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sha&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;custom_id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;::&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;embedding&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;embedding&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            UPDATE documents
            SET    embedding    = %s,
                   model_tag    = %s,
                   embedded_at  = now()
            WHERE  doc_id = %s AND sha256_hash = %s
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;MODEL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;@v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;sha&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The build step queries the same &lt;code&gt;embed_queue&lt;/code&gt; predicate as the streaming pipeline — &lt;code&gt;WHERE embedded_at IS NULL OR embedded_at &amp;lt; updated_at&lt;/code&gt;. Slow-moving or fast-moving, the change-detection SQL is identical; only the cadence differs.&lt;/li&gt;
&lt;li&gt;The batch file is JSONL — one line per row. The &lt;code&gt;custom_id&lt;/code&gt; embeds both the &lt;code&gt;doc_id&lt;/code&gt; and the &lt;code&gt;sha256_hash&lt;/code&gt;. On apply, the sha guard prevents a stale batch result from clobbering a newer real-time embed (should the corpus later add streaming on top).&lt;/li&gt;
&lt;li&gt;Submission is a two-step upload + create. The &lt;code&gt;completion_window = "24h"&lt;/code&gt; tag opts into batch-tier pricing — 50% off real-time. OpenAI processes the batch on their own schedule and posts results within 24 hours.&lt;/li&gt;
&lt;li&gt;The 06:00 poll retrieves the batch status; if &lt;code&gt;completed&lt;/code&gt;, it downloads the output JSONL and applies the vectors. If not, the poll job re-schedules itself in an hour and retries.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;sha256_hash = sha&lt;/code&gt; predicate in the UPDATE is the key idempotency guard: it ensures a batch result is only applied if the row's content hash still matches what was submitted. If a real-time job embedded the same row in the meantime, the batch result is silently dropped.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Cost per 1M rows&lt;/th&gt;
&lt;th&gt;Freshness&lt;/th&gt;
&lt;th&gt;Ops overhead&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Nightly real-time&lt;/td&gt;
&lt;td&gt;$8&lt;/td&gt;
&lt;td&gt;~24 h&lt;/td&gt;
&lt;td&gt;low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nightly batch tier&lt;/td&gt;
&lt;td&gt;$4&lt;/td&gt;
&lt;td&gt;~24 h&lt;/td&gt;
&lt;td&gt;low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Streaming CDC + real-time&lt;/td&gt;
&lt;td&gt;$8&lt;/td&gt;
&lt;td&gt;&amp;lt; 2 s&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For any corpus with freshness SLA &amp;gt; 6 hours, use the batch tier. The 50% savings are unconditional; the operational overhead is zero beyond a nightly cron.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on change detection strategies
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Your corpus is a 20-million-row product catalogue with a 1% daily change rate. Freshness SLA is 5 minutes for prices and stock, 4 hours for descriptions, 24 hours for taxonomy. Walk me through the three-tier refresh architecture, the change queues, and how you keep the cost under $100/month."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a three-tier change-detection pipeline (streaming + hourly + nightly batch)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Three-tier refresh architecture
&lt;/span&gt;
&lt;span class="c1"&gt;# Tier 1 — STREAMING (freshness 5 min)     — CDC → Kafka → real-time embed
#   scope: price + stock + availability text
# Tier 2 — HOURLY (freshness 4 h)          — queue view + real-time embed
#   scope: product description + specifications
# Tier 3 — NIGHTLY BATCH (freshness 24 h)  — queue view + batch-tier API
#   scope: taxonomy + brand + category text
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Split queue views by content field group&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;embed_queue_stream&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price_and_stock_text&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;products&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;embed_stream_at&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="k"&gt;OR&lt;/span&gt;  &lt;span class="n"&gt;embed_stream_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;price_and_stock_updated_at&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;embed_queue_hourly&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description_text&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;products&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;embed_desc_at&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="k"&gt;OR&lt;/span&gt;  &lt;span class="n"&gt;embed_desc_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;description_updated_at&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;embed_queue_nightly&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;taxonomy_text&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;products&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;embed_tax_at&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="k"&gt;OR&lt;/span&gt;  &lt;span class="n"&gt;embed_tax_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;taxonomy_updated_at&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Schedule&lt;/span&gt;
&lt;span class="na"&gt;tier_stream&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kafka topic cdc.products (Debezium)&lt;/span&gt;
  &lt;span class="na"&gt;worker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;4 consumers, batch=256, target latency 5 min&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;     &lt;span class="s"&gt;real-time text-embedding-3-small&lt;/span&gt;

&lt;span class="na"&gt;tier_hourly&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cron 0 * * * *&lt;/span&gt;
  &lt;span class="na"&gt;worker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;1 job, batch=256, target latency 4 h&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;     &lt;span class="s"&gt;real-time text-embedding-3-small&lt;/span&gt;

&lt;span class="na"&gt;tier_nightly&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cron 30 22 * * *&lt;/span&gt;
  &lt;span class="na"&gt;worker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;submit batch → poll at 06:00&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;     &lt;span class="s"&gt;OpenAI batch API text-embedding-3-small (-50%)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;th&gt;Daily rows&lt;/th&gt;
&lt;th&gt;Freshness&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Streaming&lt;/td&gt;
&lt;td&gt;price/stock text&lt;/td&gt;
&lt;td&gt;~180K&lt;/td&gt;
&lt;td&gt;5 min&lt;/td&gt;
&lt;td&gt;~$3.60/day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hourly&lt;/td&gt;
&lt;td&gt;description&lt;/td&gt;
&lt;td&gt;~15K&lt;/td&gt;
&lt;td&gt;4 h&lt;/td&gt;
&lt;td&gt;~$0.30/day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nightly batch&lt;/td&gt;
&lt;td&gt;taxonomy&lt;/td&gt;
&lt;td&gt;~5K&lt;/td&gt;
&lt;td&gt;24 h&lt;/td&gt;
&lt;td&gt;~$0.05/day (batch)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;~200K&lt;/td&gt;
&lt;td&gt;mixed&lt;/td&gt;
&lt;td&gt;~$4/day ≈ $120/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After the rollout, the pipeline hits every freshness SLA at its cheapest possible tier. The streaming layer costs the most per row but only handles the 1% of content that genuinely needs sub-minute freshness. The batch layer covers 60% of the change volume at half the price.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Surface&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Price/stock freshness p99&lt;/td&gt;
&lt;td&gt;3.5 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Description freshness p99&lt;/td&gt;
&lt;td&gt;45 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Taxonomy freshness p99&lt;/td&gt;
&lt;td&gt;22 h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total monthly OpenAI cost&lt;/td&gt;
&lt;td&gt;$118&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Old cost (single-tier real-time nightly full re-embed)&lt;/td&gt;
&lt;td&gt;$2,400&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Match freshness SLA to tier&lt;/strong&gt;&lt;/strong&gt; — not every field needs the same cadence. Splitting the queue by content-field group and running each at its cheapest tier is a 20× cost lever with zero SLA loss.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Per-field embedded_at cursor&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;embed_stream_at&lt;/code&gt;, &lt;code&gt;embed_desc_at&lt;/code&gt;, &lt;code&gt;embed_tax_at&lt;/code&gt; are independent freshness cursors. A price change re-embeds the stream-tier vector without touching the hourly or nightly ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Batch tier for the tail&lt;/strong&gt;&lt;/strong&gt; — 60% of the change volume is taxonomy that changes once a week and does not need sub-day freshness. Send it to the batch API at 50% off; save $1,200/month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;CDC only where needed&lt;/strong&gt;&lt;/strong&gt; — streaming CDC has real infra overhead (Kafka, Debezium, consumer groups). Confine it to the fields that need it. The description and taxonomy tiers can run on plain cron + Postgres.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — O(changed_rows_per_tier × per-tier price). The three-tier split matches the cost of each change event to its actual freshness requirement. The alternative (single-tier real-time on everything) is 20× more expensive with no SLA gain.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — sql&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL problems on change-queue views and CDC&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;ETL&lt;/span&gt;
&lt;span&gt;Topic — etl&lt;/span&gt;
&lt;strong&gt;ETL problems on multi-tier refresh cadence&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  3. Cost accounting — dollars per doc
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Every embedding request is billed in tokens, not documents — the senior mental model is &lt;code&gt;cost = tokens × rate × tier_multiplier&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;&lt;code&gt;embedding cost&lt;/code&gt; is &lt;code&gt;tokens × rate × tier_multiplier&lt;/code&gt;, where the rate depends on the model, the tier multiplier is 1.0 for real-time inference and 0.5 for batch, and tokens is the sum of BPE-tokenized input across every document&lt;/strong&gt;. Every other cost discussion — model choice, self-host vs managed, batch tier — is a variation on plugging different numbers into this formula.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy6fc8gwzrm16d4dsqswl.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy6fc8gwzrm16d4dsqswl.jpeg" alt="Iconographic cost diagram — a gauge dial for cost per doc, a doc-stack showing 10M rows × $0.0001, and a batch-pricing tier ladder with real-time, batch (-50%), and self-host BGE/E5, on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four "must-answer" axes for cost.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model choice.&lt;/strong&gt; OpenAI &lt;code&gt;text-embedding-3-small&lt;/code&gt; ($0.00002/1K tokens), &lt;code&gt;text-embedding-3-large&lt;/code&gt; ($0.00013/1K), Cohere &lt;code&gt;embed-v4&lt;/code&gt; ($0.00010/1K), or a self-hosted BGE-large / E5-large / GTE-large. Each has a distinct quality-per-dollar profile.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier.&lt;/strong&gt; Real-time (full price, sub-second) vs batch (half price, up to 24-hour turnaround). Refresh workloads should default to batch unless a freshness SLA forbids it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token accounting.&lt;/strong&gt; Providers bill on the BPE token count of the input, not the document count. Average English text is roughly 1.3 tokens per word; long-form docs punch above their doc count on the invoice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TCO for self-hosted.&lt;/strong&gt; GPU rental (H100, A10, L4) + engineering time + Kubernetes overhead. Cheaper only when scale justifies — typically past ~100M embeddings per month.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Model pricing snapshot (2026).&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OpenAI text-embedding-3-small.&lt;/strong&gt; 1536 dims. $0.00002 / 1K tokens. The workhorse for RAG in 2026.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenAI text-embedding-3-large.&lt;/strong&gt; 3072 dims. $0.00013 / 1K tokens. 6.5× the price for typically 5–10% better recall — worth it for legal, medical, or high-stakes retrieval; overkill for most consumer RAG.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cohere embed-v4.&lt;/strong&gt; Multiple dimension options. ~$0.00010 / 1K. Strong on multilingual and code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Voyage AI voyage-3.&lt;/strong&gt; ~$0.00006 / 1K. Competitive on English general knowledge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosted BGE-large-en-v1.5.&lt;/strong&gt; 1024 dims. Runs on a single L4 GPU at ~1000 tokens/sec/instance. TCO at scale is roughly $0.000005 / 1K including GPU + orchestration — 4× cheaper than OpenAI at ≥100M-embeddings-per-month scale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosted E5-large-v2.&lt;/strong&gt; 1024 dims. Similar profile to BGE; slightly stronger on retrieval benchmarks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Batch pricing — the unconditional 50% off.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OpenAI batch API.&lt;/strong&gt; Same models, 50% off, 24-hour completion window. For refresh workloads, this is free money.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anthropic batch API.&lt;/strong&gt; (Message inference; not embeddings, but the pattern is identical.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cohere batch mode.&lt;/strong&gt; ~50% off real-time. Enable via header on the request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosted.&lt;/strong&gt; No tier concept; you own the hardware. But you can shift work to off-peak spot instances for a similar effective discount.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Token accounting — the number you actually pay for.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rule of thumb.&lt;/strong&gt; English text is ~1.3 tokens/word or ~4 chars/token. A 400-word doc is ~520 tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chunking effect.&lt;/strong&gt; RAG systems often chunk documents into ~500-token chunks before embedding. A 4000-token doc becomes 8 embed requests — 8× the token count of a doc-level embed. Chunk strategy directly affects cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Locale multiplier.&lt;/strong&gt; Non-English text (especially CJK) tokenizes at 2–4× the token count of the equivalent English content. Bill accordingly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provider tokenizer differs.&lt;/strong&gt; OpenAI uses cl100k_base; Cohere uses its own BPE; Voyage differs again. Costs are model-specific even when the character count is identical.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Self-host TCO — when to break out of the managed API.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GPU.&lt;/strong&gt; An L4 GPU on GCP is ~$0.50/hr on-demand, ~$0.20/hr spot. One L4 embeds ~1000 tokens/sec on BGE-large.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Throughput.&lt;/strong&gt; 1000 tokens/sec × 3600 sec = 3.6M tokens/hour per GPU. 24×7 → 86M tokens/day → 2.6B tokens/month per GPU.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost.&lt;/strong&gt; $0.20/hr × 720 hr = $144/month per L4 spot instance. $144 / 2.6B tokens ≈ $0.00000005 / token = $0.00005 / 1K tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Crossover.&lt;/strong&gt; OpenAI &lt;code&gt;text-embedding-3-small&lt;/code&gt; at $0.00002 / 1K tokens vs self-host at $0.00005 / 1K on paper — but that ignores engineering time, orchestration overhead, and quality gap. Realistic crossover is ~100M embeddings/month (a very large corpus).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on cost.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"What does an embedding refresh cost for 10M docs?" — walk through the token × rate × tier math.&lt;/li&gt;
&lt;li&gt;"When would you self-host instead of using OpenAI?" — throughput past ~100M embeddings/month + engineering team with GPU/K8s ops.&lt;/li&gt;
&lt;li&gt;"What's the batch-tier discount?" — 50% off, 24-hour turnaround.&lt;/li&gt;
&lt;li&gt;"How do you account for chunked docs?" — sum tokens across chunks; chunk count is a cost multiplier.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — cost math for a 10M-doc corpus with 1 refresh per month
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A textbook cost problem. A 10-million-document corpus needs a full re-embed once per month (for a model bump, or as a periodic freshness reset). Average document is 500 tokens. Compare &lt;code&gt;text-embedding-3-small&lt;/code&gt;, &lt;code&gt;text-embedding-3-large&lt;/code&gt;, and self-hosted BGE-large on price. Show the batch-tier discount.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Total tokens.&lt;/strong&gt; 10M docs × 500 tokens = 5B tokens per full refresh.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time small.&lt;/strong&gt; 5B × $0.00002 / 1K = $100.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time large.&lt;/strong&gt; 5B × $0.00013 / 1K = $650.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch small.&lt;/strong&gt; $100 × 0.5 = $50.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-host BGE.&lt;/strong&gt; 5B tokens / 3.6M tokens/hr = 1389 GPU-hours; at $0.20/hr spot = $278. Plus engineering / orchestration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Compare the four options on price and quality, and recommend the pick for a mid-size RAG stack that refreshes monthly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Corpus&lt;/td&gt;
&lt;td&gt;10M docs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avg tokens per doc&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refresh cadence&lt;/td&gt;
&lt;td&gt;monthly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total tokens per refresh&lt;/td&gt;
&lt;td&gt;5B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L4 spot price&lt;/td&gt;
&lt;td&gt;$0.20/hr&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BGE throughput&lt;/td&gt;
&lt;td&gt;1000 tokens/sec/GPU&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;total_tokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tokens_per_doc&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;docs&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;tokens_per_doc&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;openai_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price_per_1k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;multiplier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;price_per_1k&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;multiplier&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;self_host_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tokens_per_sec_per_gpu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gpu_hourly&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;gpu_hours&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;tokens_per_sec_per_gpu&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;gpu_hours&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;gpu_hourly&lt;/span&gt;

&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;total_tokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10_000_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tokens_per_doc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OpenAI text-embedding-3-small realtime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nf"&gt;openai_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.00002&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OpenAI text-embedding-3-small batch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="nf"&gt;openai_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.00002&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OpenAI text-embedding-3-large realtime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nf"&gt;openai_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.00013&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OpenAI text-embedding-3-large batch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="nf"&gt;openai_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.00013&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Self-host BGE-large (L4 spot)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="nf"&gt;self_host_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Self-host BGE-large (L4 on-demand)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="nf"&gt;self_host_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;kv&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;kv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; $&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Expected output (order matters):
OpenAI text-embedding-3-small batch                $  50.00
OpenAI text-embedding-3-small realtime             $ 100.00
Self-host BGE-large (L4 spot)                      $ 277.78
OpenAI text-embedding-3-large batch                $ 325.00
Self-host BGE-large (L4 on-demand)                 $ 694.44
OpenAI text-embedding-3-large realtime             $ 650.00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Total tokens for one full refresh: 10M docs × 500 tokens = 5B tokens. This is the volume every provider sees; the price varies by model + tier.&lt;/li&gt;
&lt;li&gt;The cheapest option is OpenAI &lt;code&gt;text-embedding-3-small&lt;/code&gt; on the batch tier at $50. For a corpus that only needs monthly refresh, waiting 24 hours for the batch to complete costs nothing.&lt;/li&gt;
&lt;li&gt;Self-hosted BGE on L4 spot instances is $278 — cheaper than &lt;code&gt;text-embedding-3-large&lt;/code&gt; real-time but more expensive than &lt;code&gt;text-embedding-3-small&lt;/code&gt; on either tier. At this scale (5B tokens/month), self-host does not win on price.&lt;/li&gt;
&lt;li&gt;The crossover point for self-host is roughly 20B tokens/month against &lt;code&gt;text-embedding-3-small&lt;/code&gt; batch — 4× this corpus's monthly volume. Self-host only makes sense once you are re-embedding 40M+ docs per month or running a very high-throughput real-time inference workload where API rate limits become a bottleneck.&lt;/li&gt;
&lt;li&gt;Recommendation: &lt;code&gt;text-embedding-3-small&lt;/code&gt; on batch tier at $50/month. Real-time is a 2× premium for zero benefit on a monthly refresh cadence; &lt;code&gt;text-embedding-3-large&lt;/code&gt; is a 13× premium for a 5–10% quality bump — only worth it in domains where recall precision matters intensely.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Cost / month&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI 3-small batch&lt;/td&gt;
&lt;td&gt;$50&lt;/td&gt;
&lt;td&gt;Recommended for monthly refresh&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI 3-small real-time&lt;/td&gt;
&lt;td&gt;$100&lt;/td&gt;
&lt;td&gt;For sub-day freshness&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI 3-large batch&lt;/td&gt;
&lt;td&gt;$325&lt;/td&gt;
&lt;td&gt;High-stakes retrieval only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-host BGE spot&lt;/td&gt;
&lt;td&gt;$278&lt;/td&gt;
&lt;td&gt;Only wins at 4× this scale&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-host BGE on-demand&lt;/td&gt;
&lt;td&gt;$694&lt;/td&gt;
&lt;td&gt;Never wins at this scale&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For monthly full-corpus refreshes under 100M docs, &lt;code&gt;text-embedding-3-small&lt;/code&gt; on batch tier is nearly always the answer. Self-host is a decision that starts making sense only at very large corpora with sustained high throughput.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — incremental refresh vs full re-embed cost delta
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Even after picking the cheapest model + tier, the incremental-vs-full split is another 20–50× cost lever. A 10M-doc corpus with 2% daily churn under a nightly full re-embed pays for 300M docs/month; under incremental it pays for 6M/month. Show the arithmetic and the break-even that justifies the extra infra complexity of an incremental pipeline.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Full nightly.&lt;/strong&gt; 10M docs × 30 nights = 300M doc-embeddings/month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incremental daily.&lt;/strong&gt; 200K docs × 30 nights = 6M doc-embeddings/month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ratio.&lt;/strong&gt; 50× fewer tokens; 50× fewer dollars.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; For a 10M-doc corpus with 2% daily churn on &lt;code&gt;text-embedding-3-small&lt;/code&gt;, quantify the monthly cost delta between nightly full and incremental refresh. Include batch-tier savings for both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Corpus&lt;/td&gt;
&lt;td&gt;10M docs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avg tokens per doc&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Daily churn&lt;/td&gt;
&lt;td&gt;2% (200K docs)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model&lt;/td&gt;
&lt;td&gt;text-embedding-3-small ($0.00002/1K)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;monthly_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docs_per_night&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tokens_per_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price_per_1k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nights&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;tier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;
    &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;docs_per_night&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;tokens_per_doc&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;nights&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;price_per_1k&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;tier&lt;/span&gt;

&lt;span class="c1"&gt;# Full nightly re-embed
&lt;/span&gt;&lt;span class="n"&gt;full_rt&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;monthly_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10_000_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.00002&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                    &lt;span class="c1"&gt;# $3000
&lt;/span&gt;&lt;span class="n"&gt;full_batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;monthly_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10_000_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.00002&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;# $1500
&lt;/span&gt;
&lt;span class="c1"&gt;# Incremental daily re-embed
&lt;/span&gt;&lt;span class="n"&gt;inc_rt&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;monthly_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.00002&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                        &lt;span class="c1"&gt;# $60
&lt;/span&gt;&lt;span class="n"&gt;inc_batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;monthly_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.00002&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="c1"&gt;# $30
&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Nightly full real-time:      $&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;full_rt&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Nightly full batch:          $&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;full_batch&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Incremental daily real-time: $&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;inc_rt&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Incremental daily batch:     $&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;inc_batch&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Savings incremental vs full: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;inc_batch&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;full_rt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;%&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The full nightly job re-embeds 300M doc-embeddings/month regardless of change rate. At 500 tokens/doc × $0.00002/1K, that is 150B tokens × $0.00002 = $3,000/month.&lt;/li&gt;
&lt;li&gt;The batch tier alone cuts this in half to $1,500/month. Free money if freshness SLA allows.&lt;/li&gt;
&lt;li&gt;The incremental job embeds only the 200K docs that changed each day — 6M/month. That is 3B tokens × $0.00002 = $60/month real-time or $30/month on batch.&lt;/li&gt;
&lt;li&gt;Incremental on batch tier vs full on real-time: $30 vs $3,000, a 100× reduction. Even against full-on-batch ($1,500 vs $30), the incremental win is 50×.&lt;/li&gt;
&lt;li&gt;The break-even that justifies incremental infrastructure: at ~1M docs, incremental infrastructure (hash column + queue + worker) pays back in a single month of avoided cost. Under that, the operational simplicity of nightly full might still win.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Cost / month&lt;/th&gt;
&lt;th&gt;Ratio vs baseline&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Nightly full, real-time&lt;/td&gt;
&lt;td&gt;$3,000&lt;/td&gt;
&lt;td&gt;1× (baseline)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nightly full, batch&lt;/td&gt;
&lt;td&gt;$1,500&lt;/td&gt;
&lt;td&gt;2× cheaper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Incremental, real-time&lt;/td&gt;
&lt;td&gt;$60&lt;/td&gt;
&lt;td&gt;50× cheaper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Incremental, batch&lt;/td&gt;
&lt;td&gt;$30&lt;/td&gt;
&lt;td&gt;100× cheaper&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Incremental refresh is a bigger cost lever than any model or tier choice. Ship it before you argue about which model to use.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — self-host crossover for a 200M-token/month workload
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A team is considering moving from OpenAI to self-hosted BGE-large after their monthly bill crossed $2K. Compute the real crossover including engineering time, GPU orchestration, and quality delta. Not every "cheaper on paper" moment justifies the migration.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Current volume.&lt;/strong&gt; 200M tokens/month on &lt;code&gt;text-embedding-3-small&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Current cost.&lt;/strong&gt; 200M × $0.00002 / 1K = $4,000/month real-time; $2,000 batch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-host L4 spot.&lt;/strong&gt; ~$56/month per L4 GPU handles ~2.6B tokens; team's 200M fits on one L4 comfortably.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hidden costs.&lt;/strong&gt; GPU orchestration, monitoring, model updates, on-call rota.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Build the TCO table comparing OpenAI batch vs self-hosted BGE on a single L4 spot instance. Include the engineering cost of setup and ongoing ops.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Monthly tokens&lt;/td&gt;
&lt;td&gt;200M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI 3-small batch price&lt;/td&gt;
&lt;td&gt;$2,000/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L4 spot price&lt;/td&gt;
&lt;td&gt;$0.20/hr × 720 hr = $144/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Engineering setup cost&lt;/td&gt;
&lt;td&gt;3 senior-eng-weeks = ~$15K one-time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Engineering ops cost&lt;/td&gt;
&lt;td&gt;0.25 FTE ≈ $6K/month&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;openai_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;monthly_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price_per_1k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;tier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;monthly_tokens&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;price_per_1k&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;tier&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;self_host_tco&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gpu_monthly&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;eng_ops_monthly&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;setup_amort_months&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;setup_cost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15_000&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;gpu_monthly&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;eng_ops_monthly&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;setup_cost&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;setup_amort_months&lt;/span&gt;

&lt;span class="n"&gt;TOKENS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200_000_000&lt;/span&gt;

&lt;span class="n"&gt;managed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;openai_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TOKENS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.00002&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;self&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;self_host_tco&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gpu_monthly&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;144&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;eng_ops_monthly&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;6_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OpenAI 3-small batch:       $&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;managed&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/mo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Self-host BGE-large TCO:    $&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/mo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Delta:                      $&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;managed&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/mo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;On sticker price alone, self-hosted BGE looks like a slam-dunk: $144/month for the GPU vs $2,000/month for OpenAI. That is the number that gets the "let's self-host" ticket filed.&lt;/li&gt;
&lt;li&gt;But engineering ops are not zero. A GPU-backed inference service needs on-call coverage, model updates, GPU node maintenance, monitoring, and the ability to roll back a bad model deploy. Realistic ongoing cost is ~0.25 FTE, or ~$6K/month.&lt;/li&gt;
&lt;li&gt;Setup cost — model containerisation, benchmarking, latency tuning, integration with the existing pipeline — is roughly 3 senior-engineer-weeks. Amortised over 12 months, that is $1,250/month.&lt;/li&gt;
&lt;li&gt;Realistic TCO comparison: OpenAI $2,000 vs self-host $7,394 ($144 GPU + $6,000 ops + $1,250 amortised setup). Self-hosting &lt;em&gt;loses&lt;/em&gt; by 3.7×.&lt;/li&gt;
&lt;li&gt;The crossover for a mid-sized team is roughly 3B tokens/month against &lt;code&gt;text-embedding-3-small&lt;/code&gt; batch — 15× the current volume. At that scale, the OpenAI bill would be $30K/month and the self-host TCO would still be around $7K/month, a real win. Under that scale, staying on managed is the cheaper answer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Monthly cost&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI 3-small batch&lt;/td&gt;
&lt;td&gt;$2,000&lt;/td&gt;
&lt;td&gt;Simple, no ops&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-host BGE (paper)&lt;/td&gt;
&lt;td&gt;$144&lt;/td&gt;
&lt;td&gt;Ignores everything&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-host BGE (realistic TCO)&lt;/td&gt;
&lt;td&gt;$7,394&lt;/td&gt;
&lt;td&gt;GPU + 0.25 FTE + amortised setup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Crossover volume&lt;/td&gt;
&lt;td&gt;~3B tokens/month&lt;/td&gt;
&lt;td&gt;15× current&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; The paper cost of self-hosting is always alluring. The realistic TCO — including a fractional FTE and amortised setup — is what actually determines the crossover. Under ~3B tokens/month, managed APIs almost always win.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on cost accounting
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "You're on a call with the CFO who has flagged a $12K/month OpenAI embeddings bill. The corpus is 30M docs, currently refreshed nightly on &lt;code&gt;text-embedding-3-small&lt;/code&gt;. Walk me through the cost audit — what you measure, what you cut, and where the savings actually come from."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a four-lever cost audit — incremental + batch tier + right-sized model + tiered cadence
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cost audit — from $12K/month to under $500

Lever 1 — INCREMENTAL
  Measure: daily churn rate on the corpus
  Fact:    2.5% daily churn measured over 30 days
  Change:  full nightly → hash-driven incremental
  Savings: 40× reduction ($12K → $300)

Lever 2 — BATCH TIER
  Measure: freshness SLA per content field
  Fact:    corpus is documentation; 24 h freshness is fine
  Change:  real-time API → batch API (24 h window)
  Savings: 50% ($300 → $150)

Lever 3 — RIGHT-SIZED MODEL
  Measure: recall@10 on 3-small vs 3-large on golden set
  Fact:    3-small = 0.88; 3-large = 0.89; delta is noise
  Change:  keep 3-small (was already; would have saved 6.5× if on 3-large)
  Savings: n/a (was already right-sized)

Lever 4 — TIERED CADENCE
  Measure: per-field freshness requirement
  Fact:    only titles need 15-min freshness; body 4 h; taxonomy 24 h
  Change:  three-tier queue with three cadences
  Savings: another 30% on the incremental cost ($150 → $105)

Bottom line: $12K/month → $105/month, 99% saved
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Lever&lt;/th&gt;
&lt;th&gt;From&lt;/th&gt;
&lt;th&gt;To&lt;/th&gt;
&lt;th&gt;Cumulative bill&lt;/th&gt;
&lt;th&gt;Cumulative savings&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Baseline&lt;/td&gt;
&lt;td&gt;full nightly, real-time&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;$12,000&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Incremental&lt;/td&gt;
&lt;td&gt;2.5% churn queue&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;$300&lt;/td&gt;
&lt;td&gt;97.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch tier&lt;/td&gt;
&lt;td&gt;24 h window OK&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;$150&lt;/td&gt;
&lt;td&gt;98.75%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Right-sized model&lt;/td&gt;
&lt;td&gt;(already small)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;$150&lt;/td&gt;
&lt;td&gt;98.75%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tiered cadence&lt;/td&gt;
&lt;td&gt;per-field cadence&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;$105&lt;/td&gt;
&lt;td&gt;99.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After the audit, the pipeline runs 99% cheaper with no loss of retrieval quality and (if anything) better freshness on the fields that matter — because the CDC-driven streaming tier covers the sub-hour SLAs the nightly full re-embed never actually hit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Monthly OpenAI cost&lt;/td&gt;
&lt;td&gt;$12,000&lt;/td&gt;
&lt;td&gt;$105&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docs re-embedded / day&lt;/td&gt;
&lt;td&gt;30M&lt;/td&gt;
&lt;td&gt;750K (2.5% churn, mixed tier)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Freshness on titles&lt;/td&gt;
&lt;td&gt;12 h&lt;/td&gt;
&lt;td&gt;5 min (stream tier)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Freshness on body&lt;/td&gt;
&lt;td&gt;12 h&lt;/td&gt;
&lt;td&gt;4 h (hourly tier)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Freshness on taxonomy&lt;/td&gt;
&lt;td&gt;12 h&lt;/td&gt;
&lt;td&gt;24 h (nightly batch)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retrieval recall@10&lt;/td&gt;
&lt;td&gt;0.88&lt;/td&gt;
&lt;td&gt;0.88&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Incremental is the biggest lever&lt;/strong&gt;&lt;/strong&gt; — a 40× reduction in one config change. Nothing else on the cost audit comes close.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Batch tier is free savings&lt;/strong&gt;&lt;/strong&gt; — 50% off, unconditionally, if the SLA permits. Any refresh workload that does not need sub-day freshness should be on batch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Right-sizing the model&lt;/strong&gt;&lt;/strong&gt; — the recall delta between &lt;code&gt;text-embedding-3-small&lt;/code&gt; and &lt;code&gt;text-embedding-3-large&lt;/code&gt; is typically 1–5% on general knowledge, 5–10% on specialised domains. Pay for large only when the domain justifies it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Tiered cadence&lt;/strong&gt;&lt;/strong&gt; — matching the refresh cadence to the per-field freshness requirement is the last 10–30% of savings. Streaming the fields that need it; batching the fields that don't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — audit runs in a day; savings compound monthly. The four levers combine multiplicatively: 40× × 2× × 1× × 1.4× = 112× total reduction in the extreme case.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — sql&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL problems on cost accounting and token audits&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;Optimization&lt;/span&gt;
&lt;span&gt;Topic — optimization&lt;/span&gt;
&lt;strong&gt;Optimization problems on embedding cost trade-offs&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/optimization" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  4. Model versioning + migration
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Every vector needs a &lt;code&gt;model_tag&lt;/code&gt; — without it, model migrations are a full-corpus outage; with it, they are a shadow-column rollout
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;every row that stores a vector must also store the exact &lt;code&gt;model_tag&lt;/code&gt; (name + version) that produced the vector, so a mixed-model corpus is queryable and migrations are reversible&lt;/strong&gt;. Skip this column and the next time OpenAI ships a new embedding model, the team discovers there is no way to tell which vectors are on which model — and the "migration" becomes a full-corpus re-embed with no rollback path.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg289n0a975tbqprcoxd1.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg289n0a975tbqprcoxd1.jpeg" alt="Iconographic model versioning diagram — a vector table with an id, vector, and highlighted model_tag column with values 'ada-002@v1' and 'text-emb-3-small@v2', a migration ramp arrow from ada-002 to text-embedding-3-large with a dual-write chip, and a rollback card with a reverse-arrow, on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four "must-answer" axes for versioning.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Per-row model tag.&lt;/strong&gt; Store &lt;code&gt;model_tag = 'text-embedding-3-small@v1'&lt;/code&gt; on every row. The tag is the atom of versioning; every migration operation queries and updates by tag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migration strategy.&lt;/strong&gt; Full re-embed into a shadow column, dual-write for a validation window, cut over reads, drop the old column. Never in-place — you need a rollback path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dual-write.&lt;/strong&gt; During migration, every incoming write embeds against both the old and new model and writes both vectors. If the new model turns out to be worse, reads flip back to the old.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollback.&lt;/strong&gt; Keeping the old column for 2–4 weeks after cutover is cheap insurance. Once the new model is validated on production traffic, drop the column and reclaim the storage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;model_tag&lt;/code&gt; column — schema pattern.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Format.&lt;/strong&gt; &lt;code&gt;&amp;lt;model_name&amp;gt;@&amp;lt;version&amp;gt;&lt;/code&gt;. Examples: &lt;code&gt;text-embedding-ada-002@v1&lt;/code&gt;, &lt;code&gt;text-embedding-3-small@v2&lt;/code&gt;, &lt;code&gt;bge-large-en-v1.5@self&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Index.&lt;/strong&gt; &lt;code&gt;CREATE INDEX ON documents (model_tag)&lt;/code&gt; — the migration job filters by tag to find rows on the old model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dimension.&lt;/strong&gt; Different models produce different-dimensional vectors. Either store multiple vector columns (&lt;code&gt;embedding_1536&lt;/code&gt;, &lt;code&gt;embedding_3072&lt;/code&gt;) or use a wide-enough column (pgvector allows dimension mismatch across rows if the column is untyped, but for typed columns, plan the schema up front).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-model queries.&lt;/strong&gt; A retrieval query only makes sense within one &lt;code&gt;model_tag&lt;/code&gt;. Always filter &lt;code&gt;WHERE model_tag = current_model&lt;/code&gt; in retrieval SQL, or you compare a &lt;code&gt;3-small&lt;/code&gt; query vector to an &lt;code&gt;ada-002&lt;/code&gt; document vector and the cosine similarity is meaningless.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Migration strategy — four steps.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Step 1 — shadow column.&lt;/strong&gt; &lt;code&gt;ALTER TABLE documents ADD COLUMN embedding_v2 vector(1536), ADD COLUMN model_tag_v2 TEXT&lt;/code&gt;. Nothing changes in the read path yet; only the schema is prepared.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 2 — backfill.&lt;/strong&gt; Background job walks every row, re-embeds under the new model, populates &lt;code&gt;embedding_v2&lt;/code&gt; and &lt;code&gt;model_tag_v2&lt;/code&gt;. This is the expensive step — full-corpus re-embed at the new model's rate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 3 — dual-write + validation.&lt;/strong&gt; All new writes populate both &lt;code&gt;embedding&lt;/code&gt; and &lt;code&gt;embedding_v2&lt;/code&gt;. Reads still hit &lt;code&gt;embedding&lt;/code&gt; (old). Run recall@k on both against the golden query set for a validation window (7–14 days).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 4 — cutover + drop.&lt;/strong&gt; Rename &lt;code&gt;embedding → embedding_old&lt;/code&gt;, &lt;code&gt;embedding_v2 → embedding&lt;/code&gt;, update the read query to use the renamed column. Wait 2–4 weeks with the old column still present as a rollback path. Once confidence is high, &lt;code&gt;ALTER TABLE ... DROP COLUMN embedding_old&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Dual-write pattern — the "safe migration" invariant.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On every write during migration.&lt;/strong&gt; &lt;code&gt;embedding = embed(content, old_model); embedding_v2 = embed(content, new_model)&lt;/code&gt;. Two API calls per write; short-term cost overhead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cutoff.&lt;/strong&gt; Once every row has both columns populated (backfill + dual-write until all pre-migration rows are refreshed), the two vectors coexist for every row.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read flip.&lt;/strong&gt; Change the retrieval query to use &lt;code&gt;embedding_v2&lt;/code&gt;. Roll out behind a feature flag so you can flip back at request granularity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost during migration.&lt;/strong&gt; Roughly 2× steady-state cost for the migration window (backfill + dual-write). Budget for this; it is not free but it is bounded.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Rollback — the invariant that lets you sleep.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep the old column.&lt;/strong&gt; For 2–4 weeks after cutover, &lt;code&gt;embedding_old&lt;/code&gt; is untouched but present. Rollback is a one-line schema swap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version the migration.&lt;/strong&gt; Every migration in the version-control system has a name (&lt;code&gt;m025_upgrade_to_text_embedding_3_large&lt;/code&gt;); the reverse migration (&lt;code&gt;m025_down.sql&lt;/code&gt;) does the opposite. Roll forward and back like any other schema change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Behavioural check.&lt;/strong&gt; Keep the recall@k drift monitor running through the migration window. If the new model's recall drops on production traffic (not just the golden set), flip back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kill switch.&lt;/strong&gt; A feature flag at the retrieval layer (&lt;code&gt;use_new_embeddings&lt;/code&gt;) lets an on-call engineer switch back to the old vectors in &amp;lt; 60 seconds without a redeploy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on versioning.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Every vector needs what column?" — model_tag.&lt;/li&gt;
&lt;li&gt;"How do you migrate 10M vectors to a new model without downtime?" — shadow column + backfill + dual-write + cutover.&lt;/li&gt;
&lt;li&gt;"How do you roll back if the new model is worse?" — old column kept for 2–4 weeks + feature flag + swap.&lt;/li&gt;
&lt;li&gt;"What happens to a query if the corpus is mid-migration?" — filter &lt;code&gt;WHERE model_tag = current_model&lt;/code&gt;; mixed reads are meaningless.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — migrating ada-002 to text-embedding-3-large
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A 5-year-old RAG stack still runs on &lt;code&gt;text-embedding-ada-002&lt;/code&gt; (1536-d) — the OpenAI original. The team plans to migrate to &lt;code&gt;text-embedding-3-large&lt;/code&gt; (3072-d) to pick up the recall improvement on their financial-document corpus. Different dimensions means a new column, not an in-place upgrade. Walk through the four-step migration.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Old model.&lt;/strong&gt; ada-002, 1536-d, $0.00010/1K tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;New model.&lt;/strong&gt; text-embedding-3-large, 3072-d, $0.00013/1K tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Corpus.&lt;/strong&gt; 2M docs, 400 tokens/doc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migration cost.&lt;/strong&gt; 2M × 400 × $0.00013 / 1K = $104 (batch tier: $52). Trivial.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Ship the four-step migration with SQL DDL, backfill job, dual-write logic, and cutover.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Old model&lt;/td&gt;
&lt;td&gt;text-embedding-ada-002 (1536-d)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New model&lt;/td&gt;
&lt;td&gt;text-embedding-3-large (3072-d)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Corpus size&lt;/td&gt;
&lt;td&gt;2M docs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backfill tier&lt;/td&gt;
&lt;td&gt;batch API (50% off)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Step 1 — shadow column&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;
  &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;embedding_v2&lt;/span&gt;  &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3072&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;model_tag_v2&lt;/span&gt;  &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;documents_v2_hnsw&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;hnsw&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding_v2&lt;/span&gt; &lt;span class="n"&gt;vector_cosine_ops&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;embedding_v2&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 4 — cutover (executed only after validation)&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="k"&gt;RENAME&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;embedding_old&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="k"&gt;RENAME&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;model_tag&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;model_tag_old&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="k"&gt;RENAME&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;embedding_v2&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="k"&gt;RENAME&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;model_tag_v2&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;model_tag&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 4 weeks later — drop the old column&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;
  &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;embedding_old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;model_tag_old&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Step 2 — backfill via batch API
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;

&lt;span class="n"&gt;NEW_MODEL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-embedding-3-large&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_backfill_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cursor_backfill&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;itersize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        SELECT doc_id, content
        FROM   documents
        WHERE  embedding_v2 IS NULL
        ORDER  BY doc_id
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NamedTemporaryFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;suffix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delete&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;custom_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/v1/embeddings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;NEW_MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;

&lt;span class="c1"&gt;# Step 3 — dual-write for the migration window
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;embed_and_write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;old&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-embedding-ada-002&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;
    &lt;span class="n"&gt;new&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;NEW_MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                 &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        UPDATE documents
        SET    embedding    = %s,
               model_tag    = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;text-embedding-ada-002@v1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;,
               embedding_v2 = %s,
               model_tag_v2 = %s,
               embedded_at  = now()
        WHERE  doc_id = %s
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;NEW_MODEL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;@v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Step 1 (shadow column): the ALTER adds &lt;code&gt;embedding_v2&lt;/code&gt; at the new dimension (3072) and &lt;code&gt;model_tag_v2&lt;/code&gt;. Nothing in the read path changes; existing retrieval queries still hit &lt;code&gt;embedding&lt;/code&gt;. An HNSW index on &lt;code&gt;embedding_v2&lt;/code&gt; is built with a &lt;code&gt;WHERE&lt;/code&gt; clause so it only indexes populated rows during backfill.&lt;/li&gt;
&lt;li&gt;Step 2 (backfill): a background job iterates every row, submits a batch to the OpenAI batch API, and writes the results into &lt;code&gt;embedding_v2&lt;/code&gt; + &lt;code&gt;model_tag_v2&lt;/code&gt;. Batch tier makes this $52 instead of $104 for the whole 2M-doc corpus.&lt;/li&gt;
&lt;li&gt;Step 3 (dual-write + validation): during the migration window (2 weeks after backfill completes), every incoming write embeds both models. Retrieval queries still read &lt;code&gt;embedding&lt;/code&gt; but a shadow retrieval job runs the same queries against &lt;code&gt;embedding_v2&lt;/code&gt; and reports recall@k for comparison against the golden query set.&lt;/li&gt;
&lt;li&gt;Step 4 (cutover): once the shadow recall@k is proven to match or exceed the old, a single transactional rename swaps the columns. Zero-downtime — the retrieval query's &lt;code&gt;SELECT ... FROM documents WHERE embedding &amp;lt;-&amp;gt; ...&lt;/code&gt; now runs against the new vectors.&lt;/li&gt;
&lt;li&gt;Rollback: the old column is preserved for 4 weeks. If the new model shows worse behaviour on production traffic (not just the golden set), rename back — one transaction, instant rollback. After 4 weeks, drop the old column and reclaim the ~24 GB (2M × 3072 × 4 bytes) of storage.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Duration&lt;/th&gt;
&lt;th&gt;Read path&lt;/th&gt;
&lt;th&gt;Write path&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Shadow column&lt;/td&gt;
&lt;td&gt;seconds&lt;/td&gt;
&lt;td&gt;embedding&lt;/td&gt;
&lt;td&gt;embedding&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backfill&lt;/td&gt;
&lt;td&gt;24 h (batch)&lt;/td&gt;
&lt;td&gt;embedding&lt;/td&gt;
&lt;td&gt;embedding&lt;/td&gt;
&lt;td&gt;$52&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dual-write&lt;/td&gt;
&lt;td&gt;2 weeks&lt;/td&gt;
&lt;td&gt;embedding&lt;/td&gt;
&lt;td&gt;both&lt;/td&gt;
&lt;td&gt;2× normal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cutover&lt;/td&gt;
&lt;td&gt;seconds&lt;/td&gt;
&lt;td&gt;embedding_v2 (renamed)&lt;/td&gt;
&lt;td&gt;new only&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rollback window&lt;/td&gt;
&lt;td&gt;4 weeks&lt;/td&gt;
&lt;td&gt;new&lt;/td&gt;
&lt;td&gt;new&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drop old&lt;/td&gt;
&lt;td&gt;seconds&lt;/td&gt;
&lt;td&gt;new&lt;/td&gt;
&lt;td&gt;new&lt;/td&gt;
&lt;td&gt;-0 (reclaim 24 GB)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Every model migration follows the same four steps: shadow → backfill → dual-write → cutover, with rollback via column preservation. Only the model names change; the pattern is identical.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — mixed-model corpus and read-time filtering
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; During a long migration or a slow-rolling model bump, the corpus is genuinely mixed — some rows are on the old model, some on the new. Retrieval that ignores &lt;code&gt;model_tag&lt;/code&gt; compares vectors from different spaces and returns garbage. The fix is a &lt;code&gt;WHERE model_tag = current_model&lt;/code&gt; predicate on every retrieval query. Walk through the pattern.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The bug.&lt;/strong&gt; A cosine-similarity search over vectors from two different models returns semantically arbitrary results — the two model spaces are unrelated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The fix.&lt;/strong&gt; Every retrieval query filters &lt;code&gt;WHERE model_tag = current_model&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The cost.&lt;/strong&gt; A partial index on &lt;code&gt;(model_tag) WHERE model_tag = current_model&lt;/code&gt; keeps the filter free.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Show the retrieval SQL that safely queries a mid-migration corpus, plus the index that keeps it fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Corpus&lt;/td&gt;
&lt;td&gt;2M docs, mid-migration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rows on old model&lt;/td&gt;
&lt;td&gt;60%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rows on new model&lt;/td&gt;
&lt;td&gt;40%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Current model&lt;/td&gt;
&lt;td&gt;text-embedding-3-small@v2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Partial HNSW index scoped to the current model&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;documents_current_hnsw&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;hnsw&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="n"&gt;vector_cosine_ops&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;model_tag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'text-embedding-3-small@v2'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Retrieval query — always filter by model_tag&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;cosine_similarity&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;documents&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;model_tag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'text-embedding-3-small@v2'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt;  &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# App-side — pin the current model tag centrally
&lt;/span&gt;&lt;span class="n"&gt;CURRENT_MODEL_TAG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-embedding-3-small@v2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query_text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;q_vec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-embedding-3-small&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;query_text&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            SELECT doc_id, content, 1 - (embedding &amp;lt;=&amp;gt; %s::vector) AS score
            FROM   documents
            WHERE  model_tag = %s
            ORDER  BY embedding &amp;lt;=&amp;gt; %s::vector
            LIMIT  %s
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q_vec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CURRENT_MODEL_TAG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q_vec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The partial HNSW index is scoped to &lt;code&gt;WHERE model_tag = current_model&lt;/code&gt;. During migration, this index only covers the 40% of rows already on the new model; queries against those rows are as fast as if the whole corpus were on the new model.&lt;/li&gt;
&lt;li&gt;The retrieval SQL always includes &lt;code&gt;WHERE model_tag = current_model&lt;/code&gt;. This guarantees the vector-space math is meaningful — comparing a &lt;code&gt;3-small&lt;/code&gt; query vector to a &lt;code&gt;3-small&lt;/code&gt; document vector, never cross-model.&lt;/li&gt;
&lt;li&gt;During migration, a query might return fewer than 10 results because the model-filtered subset is smaller. This is acceptable — better to return N &amp;lt; 10 correct results than 10 semantically meaningless ones.&lt;/li&gt;
&lt;li&gt;Rows not yet on the current model are effectively invisible to retrieval until the backfill completes. This is the trade-off of a long migration: some content is temporarily un-retrievable. Prioritise the backfill order (e.g., by traffic recency) to minimise this window.&lt;/li&gt;
&lt;li&gt;Once migration is complete and &lt;code&gt;model_tag = current_model&lt;/code&gt; for every row, the partial index becomes a full index and the migration filter becomes a no-op (though the SQL still includes it for safety on the next migration).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query&lt;/th&gt;
&lt;th&gt;Rows returned&lt;/th&gt;
&lt;th&gt;Correctness&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No filter&lt;/td&gt;
&lt;td&gt;10 (mixed model)&lt;/td&gt;
&lt;td&gt;garbage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;model_tag filter (during migration)&lt;/td&gt;
&lt;td&gt;up to 10 (40% subset)&lt;/td&gt;
&lt;td&gt;correct&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;model_tag filter (post migration)&lt;/td&gt;
&lt;td&gt;10 (100% subset)&lt;/td&gt;
&lt;td&gt;correct&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Every retrieval query in a production pipeline should include &lt;code&gt;WHERE model_tag = current_model&lt;/code&gt; — even if the corpus is currently on a single model. It costs nothing when the corpus is unified and it saves the migration.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — rollback path when a new model regresses
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A team migrates a 10M-doc corpus from &lt;code&gt;text-embedding-3-small&lt;/code&gt; to &lt;code&gt;text-embedding-3-large&lt;/code&gt; for the recall gain. Three days after cutover, the on-call product engineer reports "search results feel less relevant" — anecdotal but consistent. The golden set doesn't show the regression, but production traffic does. Walk through the rollback with a feature flag + column preservation.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The signal.&lt;/strong&gt; User feedback / click-through rate drop on production traffic; golden set is clean.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The suspect.&lt;/strong&gt; A domain shift on production queries the golden set does not cover.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The rollback.&lt;/strong&gt; Flip feature flag; retrieval goes back to &lt;code&gt;embedding_old&lt;/code&gt;; investigate offline.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Implement the feature-flag-gated retrieval that lets on-call flip back to the old vectors in under 60 seconds without a redeploy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Feature flag&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;use_new_embeddings&lt;/code&gt; (LaunchDarkly / Unleash / Postgres row)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Old column&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;embedding_old&lt;/code&gt; (kept for 4 weeks)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New column&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;embedding&lt;/code&gt; (current)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rollback SLA&lt;/td&gt;
&lt;td&gt;60 seconds from decision to effect&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;current_flag&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Simplest: read from a small config table; cache 5 s
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;feature_flag_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;use_new_embeddings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query_text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;use_new&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;current_flag&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;use_new&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;q_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-embedding-3-large&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;col&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;embedding&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;tag_col&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model_tag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;tag_val&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-embedding-3-large@v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;q_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-embedding-3-small&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;col&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;embedding_old&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;tag_col&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model_tag_old&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;tag_val&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-embedding-3-small@v2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;q_vec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;q_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;query_text&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;

    &lt;span class="n"&gt;sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        SELECT doc_id, content, 1 - (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &amp;lt;=&amp;gt; %s::vector) AS score
        FROM   documents
        WHERE  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tag_col&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; = %s
        ORDER  BY &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &amp;lt;=&amp;gt; %s::vector
        LIMIT  %s
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q_vec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tag_val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q_vec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Rollback runbook — one command&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;feature_flags&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt;    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'use_new_embeddings'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Retrieval now hits embedding_old with the old query model&lt;/span&gt;
&lt;span class="c1"&gt;-- Old column is intact for 4 weeks; roll-forward is the reverse UPDATE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The feature flag is evaluated per-request with a 5-second cache — a UPDATE to the flag table propagates to all app servers within 5 seconds. Rollback effect is under 60 seconds without a code deploy.&lt;/li&gt;
&lt;li&gt;When the flag is TRUE (default), the retrieval query uses &lt;code&gt;embedding&lt;/code&gt; (new column) with the new model's query vector. When the flag is FALSE, it uses &lt;code&gt;embedding_old&lt;/code&gt; with the old model's query vector. Both paths coexist because the migration preserved both columns.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;model_tag_old&lt;/code&gt; predicate is critical: it filters the old-column query to only rows still on the old model. During the 4-week rollback window, &lt;code&gt;model_tag_old&lt;/code&gt; values on rows written during the new-model era are NULL — those rows are correctly filtered out.&lt;/li&gt;
&lt;li&gt;On rollback, the on-call flips the flag, watches retrieval quality metrics for 15 minutes, and if the click-through rate recovers, files a ticket to investigate why the golden set missed the regression. If it does not recover, the problem was not the model change.&lt;/li&gt;
&lt;li&gt;Once the investigation completes and the new model is either fixed or accepted, either roll forward (flag → TRUE) or accept the rollback and skip the migration until a better model ships. The 4-week rollback window is enough to make an informed decision.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Flag value&lt;/th&gt;
&lt;th&gt;Read path&lt;/th&gt;
&lt;th&gt;Rollback time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pre-migration&lt;/td&gt;
&lt;td&gt;TRUE (only column)&lt;/td&gt;
&lt;td&gt;embedding (old-model)&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Post-migration&lt;/td&gt;
&lt;td&gt;TRUE&lt;/td&gt;
&lt;td&gt;embedding (new-model)&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regression detected&lt;/td&gt;
&lt;td&gt;FLIP to FALSE&lt;/td&gt;
&lt;td&gt;embedding_old (old-model)&lt;/td&gt;
&lt;td&gt;&amp;lt; 60 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Roll-forward after fix&lt;/td&gt;
&lt;td&gt;FLIP to TRUE&lt;/td&gt;
&lt;td&gt;embedding (new-model)&lt;/td&gt;
&lt;td&gt;&amp;lt; 60 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Never migrate without a feature-flag-gated read path. The dual-write + column preservation makes the flag possible; without it, rollback is a redeploy or a full re-embed. Sixty-second rollback is the standard for any production retrieval system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on model versioning
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "You have a 20M-doc RAG stack on &lt;code&gt;text-embedding-ada-002&lt;/code&gt;. Your CTO wants to migrate to &lt;code&gt;text-embedding-3-large&lt;/code&gt; for the quality improvement. Walk me through the four-step migration plan, how you'd budget the cost, and what the rollback story looks like."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using shadow-column migration with dual-write, feature-flag cutover, and 4-week rollback window
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;20M-doc migration plan — ada-002 → text-embedding-3-large

Week 1 — Schema
  ALTER TABLE documents ADD embedding_v2 vector(3072), model_tag_v2 TEXT
  CREATE partial HNSW index on embedding_v2

Week 2–3 — Backfill (batch API)
  Submit 20M rows to OpenAI batch API in daily chunks of 3M
  Total cost: 20M × 400 tokens × $0.00013/1K × 0.5 (batch) = $520
  Total wall clock: ~7 days

Week 4 — Dual-write validation
  Every incoming write embeds both models
  Shadow retrieval job scores golden set + production query sample on both
  Compare recall@10; require ≥ parity for cutover

Week 5 — Cutover
  Rename columns transactionally
  Retrieval query now uses embedding (new)
  Feature flag `use_new_embeddings` defaults TRUE

Weeks 5–8 — Rollback window
  Keep embedding_old for 4 weeks
  Monitor production traffic recall + click-through
  Flip flag if any regression detected

Week 9 — Drop old column
  ALTER TABLE documents DROP embedding_old, DROP model_tag_old
  Reclaim ~120 GB storage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Week&lt;/th&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Rollback exposure&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Shadow column&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2–3&lt;/td&gt;
&lt;td&gt;Backfill&lt;/td&gt;
&lt;td&gt;$520&lt;/td&gt;
&lt;td&gt;full — no cutover yet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Dual-write + validate&lt;/td&gt;
&lt;td&gt;2× write cost&lt;/td&gt;
&lt;td&gt;full — reads still on old&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Cutover&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;flag flip → old&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5–8&lt;/td&gt;
&lt;td&gt;Rollback window&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;flag flip → old&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Drop old column&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;none — migration final&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After the 9-week rollout, the entire 20M-doc corpus is on &lt;code&gt;text-embedding-3-large&lt;/code&gt;, storage is reclaimed, and retrieval recall@10 has improved from 0.87 to 0.91 on the golden set. The total OpenAI cost of the migration was $520 for the backfill plus ~$40 for the dual-write window — negligible against the recall gain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Surface&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Corpus size&lt;/td&gt;
&lt;td&gt;20M docs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Migration wall clock&lt;/td&gt;
&lt;td&gt;9 weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Migration cost&lt;/td&gt;
&lt;td&gt;$560 total&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recall@10 before&lt;/td&gt;
&lt;td&gt;0.87&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recall@10 after&lt;/td&gt;
&lt;td&gt;0.91&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rollback SLA achieved&lt;/td&gt;
&lt;td&gt;60 s (never exercised)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Shadow column, not in-place&lt;/strong&gt;&lt;/strong&gt; — new dimension means new column. Never truncate the old vector to force in-place; you lose the rollback path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Batch tier for backfill&lt;/strong&gt;&lt;/strong&gt; — the whole 20M-doc backfill is $520 on batch vs $1,040 real-time. Batch is the right tier for a one-shot migration where 24-hour turnaround is fine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Dual-write validation window&lt;/strong&gt;&lt;/strong&gt; — a week of dual-writes plus a shadow retrieval job on production queries catches regressions the golden set misses. This is where the "does the new model actually improve quality?" question gets answered on real traffic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Feature-flag cutover&lt;/strong&gt;&lt;/strong&gt; — a single UPDATE against a flag table is safer than a code deploy. Sixty-second rollback is the standard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — total migration budget is $560 for a 20M-doc corpus. The recall improvement pays back in engagement lift within days. Migrations are cheap; migrations without rollback paths are the expensive ones.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — sql&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL problems on model_tag columns and migration DDL&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;ETL&lt;/span&gt;
&lt;span&gt;Topic — etl&lt;/span&gt;
&lt;strong&gt;ETL problems on shadow-column backfill jobs&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  5. Drift monitoring + freshness
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Retrieval quality decays silently — a golden query set + recall@k trend is the only signal that catches drift before users do
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;&lt;code&gt;embedding drift&lt;/code&gt; is the slow decay of retrieval quality caused by corpus vocabulary shifts, query-distribution shifts, or model staleness, and the only reliable way to detect it is a golden query set scored on recall@k as a &lt;em&gt;trend&lt;/em&gt;, not a point-in-time snapshot&lt;/strong&gt;. Every other drift-monitor pattern — cluster silhouette scores, embedding-norm histograms, ANN recall probes — is either a proxy for or a supplement to the recall@k trend.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj3vbcghgak2smomm0lrx.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj3vbcghgak2smomm0lrx.jpeg" alt="Iconographic drift monitoring diagram — a golden query-set card with 5 query-chip rows and expected-doc glyphs, a trend-chart card showing recall@k dropping from 0.92 to 0.78 with a red threshold band under 0.85, and an alert card with a red flag reading 'drift alert · re-embed', on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four "must-answer" axes for drift.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Drift signal.&lt;/strong&gt; Recall@k against a golden query set. k is typically 5 or 10; the golden set is 100–500 hand-curated query→relevant-doc pairs. Trend over time, not a single value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Golden query set.&lt;/strong&gt; Curated by product / domain experts. Covers head queries (top 20% of traffic), long tail (uncommon but important intents), and adversarial cases (queries that were previously failures).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Freshness SLA per corpus tier.&lt;/strong&gt; Head content (product page titles) — minutes. Body content (descriptions, docs) — hours. Long tail (archived content) — days. Alert on freshness violations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Escalation.&lt;/strong&gt; When drift crosses threshold, the on-call runbook is: reproduce → determine if content-drift or model-drift → re-embed the affected corpus tier or begin a model migration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Building the golden query set.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sourcing.&lt;/strong&gt; 100 head queries from production logs (top 20% of traffic), 200 long-tail queries sampled across corpora, 50 hand-crafted adversarial cases where retrieval previously regressed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Labelling.&lt;/strong&gt; For each query, mark 1–3 documents as "relevant" (correct answer). Use domain experts; treat this as a data-labelling task with quality review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refresh cadence.&lt;/strong&gt; Add ~10% new queries every quarter to reflect current traffic shape. Retire stale queries whose expected docs no longer exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coverage.&lt;/strong&gt; Ensure the golden set covers every content vertical (products, docs, help centre, blog, etc.) and every language the system supports.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Recall@k — the metric that matters.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Definition.&lt;/strong&gt; For each golden query, count how many of the labelled-relevant docs appear in the top-k results. Average across queries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;k choice.&lt;/strong&gt; k=10 is standard for RAG. k=5 for higher-precision surfaces; k=20 for exploratory search.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Threshold.&lt;/strong&gt; Alert when the trailing 7-day average drops below the 30-day baseline by more than 5% relative (e.g. 0.90 → 0.85).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not just recall.&lt;/strong&gt; Also track MRR (mean reciprocal rank) — did the correct doc appear at position 1? Recall@k captures presence; MRR captures ranking quality.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Freshness SLA per corpus tier.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Head tier.&lt;/strong&gt; Product titles, prices, availability. Freshness SLA: 5 minutes. Any row where &lt;code&gt;now() - embedded_at &amp;gt; 5 minutes&lt;/code&gt; is a violation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Body tier.&lt;/strong&gt; Product descriptions, help articles, docs. Freshness SLA: 4 hours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long-tail tier.&lt;/strong&gt; Archived content, historical records. Freshness SLA: 24 hours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alert.&lt;/strong&gt; Percentage of rows past their tier's freshness SLA. Aim &amp;lt; 1%; page at &amp;gt; 5%.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Escalation runbook when drift fires.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Step 1.&lt;/strong&gt; Reproduce — run the alerting query batch offline; confirm the drop is real, not a monitoring artefact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 2.&lt;/strong&gt; Split — is the drop concentrated in one corpus tier, one language, one query cluster? Grouped recall@k analysis pinpoints the affected slice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 3.&lt;/strong&gt; Classify — content drift (corpus vocabulary shifted) or model drift (query distribution shifted away from what the model was trained on)?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 4a — content drift.&lt;/strong&gt; Trigger a re-embed of the affected slice on the current model; verify recall recovers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 4b — model drift.&lt;/strong&gt; Begin a model-migration plan to a newer embedding model; run the shadow-column pattern.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 5.&lt;/strong&gt; Post-mortem — expand the golden set with the queries that surfaced the drift.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on drift.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"How do you detect embedding drift?" — golden query set + recall@k trend, not a point-in-time metric.&lt;/li&gt;
&lt;li&gt;"How big should the golden set be?" — 100–500 pairs; refreshed quarterly.&lt;/li&gt;
&lt;li&gt;"What threshold do you alert on?" — 5% relative drop in trailing 7-day recall@k vs 30-day baseline.&lt;/li&gt;
&lt;li&gt;"Content drift or model drift — how do you tell?" — grouped analysis by corpus slice + model version.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — building the golden query set and nightly recall@k monitor
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The concrete implementation of a drift monitor: a &lt;code&gt;golden_queries&lt;/code&gt; table with query text and expected doc IDs, a nightly job that runs each query through retrieval, computes recall@10, writes the result to a &lt;code&gt;recall_at_k_history&lt;/code&gt; table, and alerts if the trend drops. Walk through the schema and the runner.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Golden set.&lt;/strong&gt; 200 hand-labelled query → relevant doc IDs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cadence.&lt;/strong&gt; Nightly job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metric.&lt;/strong&gt; Recall@10 averaged across the golden set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage.&lt;/strong&gt; History table for trend visualisation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Design the schema, the nightly runner, and the alert threshold for a drift monitor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Golden set size&lt;/td&gt;
&lt;td&gt;200 queries × 1–3 relevant docs each&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;k&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cadence&lt;/td&gt;
&lt;td&gt;nightly 03:00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alert&lt;/td&gt;
&lt;td&gt;trailing 7d avg &amp;lt; 30d avg × 0.95&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;golden_queries&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;query_id&lt;/span&gt;     &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;query_text&lt;/span&gt;   &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;relevant_ids&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;corpus_tier&lt;/span&gt;  &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;-- 'head', 'body', 'longtail'&lt;/span&gt;
  &lt;span class="k"&gt;language&lt;/span&gt;     &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt;   &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;retired_at&lt;/span&gt;   &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;recall_at_k_history&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;run_id&lt;/span&gt;       &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ran_at&lt;/span&gt;       &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;model_tag&lt;/span&gt;    &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;corpus_tier&lt;/span&gt;  &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;language&lt;/span&gt;     &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;k&lt;/span&gt;            &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;recall&lt;/span&gt;       &lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;mrr&lt;/span&gt;          &lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;query_count&lt;/span&gt;  &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Nightly runner — 03:00 UTC
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;statistics&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;mean&lt;/span&gt;

&lt;span class="n"&gt;MODEL&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-embedding-3-small&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;MODEL_TAG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;MODEL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;@v2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;K&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;retrieve_top_k&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q_vec&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        SELECT doc_id
        FROM   documents
        WHERE  model_tag = %s
        ORDER  BY embedding &amp;lt;=&amp;gt; %s::vector
        LIMIT  %s
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MODEL_TAG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q_vec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;K&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;recall_and_mrr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retrieved&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;relevant&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;relevant_set&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;relevant&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;hit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retrieved&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;relevant_set&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;recall&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retrieved&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;relevant_set&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;relevant_set&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;mrr&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;recall&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mrr&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_drift_monitor&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgres://drift@db.internal/rag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT query_id, query_text, relevant_ids, corpus_tier, language FROM golden_queries WHERE retired_at IS NULL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;scores_by_tier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;qid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qtext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lang&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;q_vec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;qtext&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;
        &lt;span class="n"&gt;top&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;retrieve_top_k&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q_vec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;recall&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mrr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;recall_and_mrr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;scores_by_tier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;recall&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mrr&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pairs&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;scores_by_tier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;avg_recall&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;avg_mrr&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            INSERT INTO recall_at_k_history
              (model_tag, corpus_tier, k, recall, mrr, query_count)
            VALUES (%s, %s, %s, %s, %s, %s)
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MODEL_TAG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;K&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;avg_recall&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;avg_mrr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        WITH recent AS (
          SELECT corpus_tier, AVG(recall) AS r7
          FROM   recall_at_k_history
          WHERE  ran_at &amp;gt; now() - INTERVAL &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;7 days&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
          GROUP  BY corpus_tier
        ),
        baseline AS (
          SELECT corpus_tier, AVG(recall) AS r30
          FROM   recall_at_k_history
          WHERE  ran_at BETWEEN now() - INTERVAL &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;30 days&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; AND now() - INTERVAL &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;7 days&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
          GROUP  BY corpus_tier
        )
        SELECT b.corpus_tier, r.r7, b.r30
        FROM   recent r
        JOIN   baseline b USING (corpus_tier)
        WHERE  r.r7 &amp;lt; b.r30 * 0.95
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r30&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="nf"&gt;page_oncall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;embedding drift on &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tier&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: 7d=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r7&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; vs 30d=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r30&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;golden_queries&lt;/code&gt; table stores the curated set with &lt;code&gt;corpus_tier&lt;/code&gt;, &lt;code&gt;language&lt;/code&gt;, and a &lt;code&gt;retired_at&lt;/code&gt; column that lets stale queries be soft-deleted without a hard row delete.&lt;/li&gt;
&lt;li&gt;The nightly runner iterates every non-retired query, embeds the query text with the current model, retrieves the top-10 documents from pgvector, and computes recall@10 and MRR against the labelled relevant_ids.&lt;/li&gt;
&lt;li&gt;Results are grouped by &lt;code&gt;corpus_tier&lt;/code&gt; so head-tier and long-tail drift can be tracked independently — a drift concentrated in one tier is a signal about that tier's content, not the model overall.&lt;/li&gt;
&lt;li&gt;Each nightly result is written to &lt;code&gt;recall_at_k_history&lt;/code&gt; — the trend table. Grafana or a simple dashboard plots the trend by tier, model_tag, and language.&lt;/li&gt;
&lt;li&gt;The alert query compares the trailing 7-day average to the 30-day baseline; a 5% relative drop pages the on-call. The 7d vs 30d comparison filters out day-to-day noise while catching genuine regressions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;30d baseline&lt;/th&gt;
&lt;th&gt;7d recent&lt;/th&gt;
&lt;th&gt;Alert?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;head&lt;/td&gt;
&lt;td&gt;0.94&lt;/td&gt;
&lt;td&gt;0.93&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;body&lt;/td&gt;
&lt;td&gt;0.88&lt;/td&gt;
&lt;td&gt;0.85&lt;/td&gt;
&lt;td&gt;yes (5% drop)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;longtail&lt;/td&gt;
&lt;td&gt;0.75&lt;/td&gt;
&lt;td&gt;0.72&lt;/td&gt;
&lt;td&gt;yes (4% drop → warn)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Drift is a trend, not a snapshot. Ship the history table and the trend chart before you obsess over the absolute recall number.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — freshness SLA violations per corpus tier
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Recall@k catches quality drift; a separate freshness monitor catches the case where the pipeline stalls and rows go stale. Track &lt;code&gt;now() - embedded_at&lt;/code&gt; per tier and alert when the fraction of stale rows exceeds a per-tier threshold. This is the "did the pipeline actually run last night?" monitor.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Head tier.&lt;/strong&gt; SLA: &lt;code&gt;embedded_at &amp;gt; now() - 5 minutes&lt;/code&gt; for 99% of rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Body tier.&lt;/strong&gt; SLA: &lt;code&gt;embedded_at &amp;gt; now() - 4 hours&lt;/code&gt; for 99% of rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long-tail tier.&lt;/strong&gt; SLA: &lt;code&gt;embedded_at &amp;gt; now() - 24 hours&lt;/code&gt; for 99% of rows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the SQL that computes freshness violation rate per tier and the alert threshold.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Freshness SLA&lt;/th&gt;
&lt;th&gt;Alert threshold&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;head&lt;/td&gt;
&lt;td&gt;5 min&lt;/td&gt;
&lt;td&gt;&amp;gt; 5% violated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;body&lt;/td&gt;
&lt;td&gt;4 h&lt;/td&gt;
&lt;td&gt;&amp;gt; 5% violated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;longtail&lt;/td&gt;
&lt;td&gt;24 h&lt;/td&gt;
&lt;td&gt;&amp;gt; 5% violated&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;freshness_by_tier&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;corpus_tier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;corpus_tier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'head'&lt;/span&gt;     &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;embedded_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'5 minutes'&lt;/span&gt;  &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;corpus_tier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'body'&lt;/span&gt;     &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;embedded_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'4 hours'&lt;/span&gt;    &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;corpus_tier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'longtail'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;embedded_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'24 hours'&lt;/span&gt;   &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;embedded_at&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;                                                        &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
      &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;violated_rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt;
          &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;corpus_tier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'head'&lt;/span&gt;     &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;embedded_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'5 minutes'&lt;/span&gt;  &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
          &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;corpus_tier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'body'&lt;/span&gt;     &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;embedded_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'4 hours'&lt;/span&gt;    &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
          &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;corpus_tier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'longtail'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;embedded_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'24 hours'&lt;/span&gt;   &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
          &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;embedded_at&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;                                                        &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
          &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;NUMERIC&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="mi"&gt;2&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;violation_pct&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;documents&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;corpus_tier&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Alert query — anything &amp;gt; 5% violation triggers&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;corpus_tier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;violation_pct&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;freshness_by_tier&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;violation_pct&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Alert scraper — every 5 min
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;scrape_freshness&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgres://drift@db.internal/rag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT corpus_tier, violation_pct FROM freshness_by_tier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pct&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="nf"&gt;emit_metric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;embed.freshness_violation_pct.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tier&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pct&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;5.0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;page_oncall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;freshness SLA violation on &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tier&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pct&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;% stale&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;freshness_by_tier&lt;/code&gt; view computes, per tier, the total row count and the count of rows past their tier's SLA. The CASE arms encode the per-tier threshold; the aggregation is a single scan.&lt;/li&gt;
&lt;li&gt;The 5% alert threshold assumes some rows will always be transiently past SLA (mid-refresh). Anything past 5% is a signal that the refresh pipeline has stalled or is unable to keep up with churn.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;embedded_at IS NULL&lt;/code&gt; is counted as a violation — a row with no vector is a row the retrieval layer cannot use. This catches new documents that were ingested but never embedded.&lt;/li&gt;
&lt;li&gt;The scraper runs every 5 minutes (matching the head tier's SLA cadence). Head-tier stalls are caught within 10 minutes; body and long-tail stalls have longer natural intervals.&lt;/li&gt;
&lt;li&gt;When the alert fires, the on-call runbook is: (a) check the change-queue depth (&lt;code&gt;SELECT COUNT(*) FROM embed_queue&lt;/code&gt;); (b) check the worker's logs for API errors; (c) check the OpenAI status page for provider incidents; (d) if the queue is deep and workers are healthy, scale out the workers.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Total&lt;/th&gt;
&lt;th&gt;Violated&lt;/th&gt;
&lt;th&gt;Violation %&lt;/th&gt;
&lt;th&gt;Alert?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;head&lt;/td&gt;
&lt;td&gt;500,000&lt;/td&gt;
&lt;td&gt;3,000&lt;/td&gt;
&lt;td&gt;0.6%&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;body&lt;/td&gt;
&lt;td&gt;3,000,000&lt;/td&gt;
&lt;td&gt;220,000&lt;/td&gt;
&lt;td&gt;7.3%&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;longtail&lt;/td&gt;
&lt;td&gt;6,500,000&lt;/td&gt;
&lt;td&gt;65,000&lt;/td&gt;
&lt;td&gt;1.0%&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Ship the freshness monitor before the recall monitor. Freshness catches the pipeline-stalled case; recall catches the pipeline-running-but-degrading case. Together they cover the whole failure surface.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — root-causing a recall@k drop after a corpus expansion
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A synthetic incident: recall@10 on the body tier drops from 0.88 to 0.82 over 10 days. No model changes; the pipeline is healthy. Investigation shows the corpus grew 30% due to a bulk import of a new product line. The new vocabulary was outside the golden set's coverage. Walk through the diagnosis and the fix.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The signal.&lt;/strong&gt; Recall@10 body-tier drop of 6 points over 10 days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The suspect.&lt;/strong&gt; Corpus content drift — new vocabulary the golden set does not cover.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The fix.&lt;/strong&gt; Expand the golden set + re-verify.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Walk through the diagnostic queries and the golden-set expansion process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Recall@10 body-tier (30d avg)&lt;/td&gt;
&lt;td&gt;0.88&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recall@10 body-tier (7d avg)&lt;/td&gt;
&lt;td&gt;0.82&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Corpus size 10d ago&lt;/td&gt;
&lt;td&gt;6M rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Corpus size today&lt;/td&gt;
&lt;td&gt;7.8M rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ingestion event&lt;/td&gt;
&lt;td&gt;+1.8M rows in a bulk import of new product line&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Diagnostic 1 — did the corpus grow?&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;DATE_TRUNC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'day'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;embedded_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;new_rows&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;documents&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;corpus_tier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'body'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt;  &lt;span class="n"&gt;embedded_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'14 days'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Diagnostic 2 — recall@10 broken down by golden-query cluster&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;gq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;corpus_tier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;gq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;language&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;queries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;recall&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;avg_recall&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;golden_queries&lt;/span&gt; &lt;span class="n"&gt;gq&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt;   &lt;span class="n"&gt;recall_at_k_history&lt;/span&gt; &lt;span class="n"&gt;rh&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;rh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;corpus_tier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;corpus_tier&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;rh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ran_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'7 days'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt;  &lt;span class="n"&gt;gq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retired_at&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;gq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;corpus_tier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;language&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Diagnostic 3 — new-product-line queries that likely miss&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="k"&gt;SUBSTRING&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;snippet&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;documents&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;corpus_tier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'body'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt;  &lt;span class="n"&gt;embedded_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'14 days'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt;  &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%new-product-line%'&lt;/span&gt;   &lt;span class="c1"&gt;-- domain-specific&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt;  &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Golden set expansion — add 30 queries from the new domain
&lt;/span&gt;&lt;span class="n"&gt;new_pairs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;how do I set up the new-product-line-model-x&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;12034&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12089&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new-product-line-model-x troubleshooting steps&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;12080&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12093&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="c1"&gt;# ... 28 more
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;expand_golden_set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lang&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgres://drift@db.internal/rag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;query_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;relevant_ids&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            INSERT INTO golden_queries (query_text, relevant_ids, corpus_tier, language)
            VALUES (%s, %s, %s, %s)
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;relevant_ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Diagnostic 1 confirms the corpus grew 30% in the last 10 days, concentrated on a specific ingestion day. This is a strong signal that the drop coincides with new content.&lt;/li&gt;
&lt;li&gt;Diagnostic 2 shows the drop is concentrated in the &lt;code&gt;body/en&lt;/code&gt; slice — not language-wide, not tier-wide. Something specific to English body content.&lt;/li&gt;
&lt;li&gt;Diagnostic 3 samples the new content and confirms it is a new product line with vocabulary the pipeline had never seen before.&lt;/li&gt;
&lt;li&gt;The root cause: the golden set was calibrated on the pre-expansion corpus. Post-expansion, queries about the new product line have no golden entries, and the queries the golden set does have now compete against 30% more documents — some of which are marginally similar to the golden queries and displace true relevant docs from the top-10.&lt;/li&gt;
&lt;li&gt;The fix: expand the golden set with 30 queries covering the new product line, plus verify the recall metric on the expanded set. If recall recovers, drift was a coverage gap. If not, the model itself has degraded on the new vocabulary and a model migration is the answer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Diagnostic&lt;/th&gt;
&lt;th&gt;Finding&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Corpus size&lt;/td&gt;
&lt;td&gt;+30% in last 10 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recall by slice&lt;/td&gt;
&lt;td&gt;body/en dropped, others stable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content sample&lt;/td&gt;
&lt;td&gt;new product line vocabulary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Root cause&lt;/td&gt;
&lt;td&gt;golden-set coverage gap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fix&lt;/td&gt;
&lt;td&gt;expand golden set + rerun monitor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recall after fix&lt;/td&gt;
&lt;td&gt;0.87 (recovered)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Every corpus expansion needs a corresponding golden-set expansion. A drift alert that coincides with a corpus growth event is usually a coverage gap, not a model failure. Check the golden set first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on drift monitoring
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "You have a production RAG pipeline that has been stable for a year. Suddenly the CS team is filing bugs about search quality. Walk me through your first-24-hour investigation — what you measure, what you rule in and out, and where the fix likely lives."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a golden-set trend, a freshness scan, and a corpus growth diff
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;First-24-hour drift investigation runbook

Hour 0-1 — Confirm the signal
  1. Sample 20 recent CS bug reports
  2. Reproduce each query against production
  3. Compare top-10 results to CS's expected answers
  4. Score: what fraction of CS bugs are genuine misses?

Hour 1-4 — Rule out infra
  1. Freshness monitor: any tier over 5% violation?
  2. Change-queue depth: is the pipeline behind?
  3. OpenAI status page: any provider incidents?
  4. Vector store health: HNSW index rebuild? Recent reindex?

Hour 4-8 — Golden-set trend
  1. Pull recall@k history for last 90 days
  2. Overlay corpus size, model version, index rebuild events
  3. Identify the inflection point — when did the drop start?
  4. Group by corpus tier and language to localise

Hour 8-12 — Content vs model classification
  1. If drop is in one tier + one language: content drift (coverage or bulk ingest)
  2. If drop is uniform: model drift (query distribution shifted)
  3. If drop is bimodal (queries split): mixed — needs both fixes

Hour 12-24 — Immediate fix + follow-up
  Content drift → expand golden set + re-embed affected slice + verify recovery
  Model drift  → begin shadow-column migration plan; feature-flag rollback ready
  Post-mortem  → add new golden queries; update runbook; freshness alert calibration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hour&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Rule in / out&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0-1&lt;/td&gt;
&lt;td&gt;CS bug reproduction&lt;/td&gt;
&lt;td&gt;Confirm real vs perception&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1-4&lt;/td&gt;
&lt;td&gt;Infra scan (freshness / queue / provider)&lt;/td&gt;
&lt;td&gt;Rule out pipeline stall&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4-8&lt;/td&gt;
&lt;td&gt;Golden-set trend&lt;/td&gt;
&lt;td&gt;Localise the drop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8-12&lt;/td&gt;
&lt;td&gt;Content vs model classify&lt;/td&gt;
&lt;td&gt;Pick the right fix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12-24&lt;/td&gt;
&lt;td&gt;Fix + follow-up&lt;/td&gt;
&lt;td&gt;Recovery + prevention&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After the 24-hour investigation, the team has a documented root cause, a fix in flight, and a golden-set expansion queued. The runbook itself is the artefact — the next drift alert takes 4 hours instead of 24 because the questions are already answered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Finding&lt;/th&gt;
&lt;th&gt;Root cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Freshness&lt;/td&gt;
&lt;td&gt;0.5% (clean)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Queue depth&lt;/td&gt;
&lt;td&gt;40 (normal)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Provider&lt;/td&gt;
&lt;td&gt;no incident&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trend inflection&lt;/td&gt;
&lt;td&gt;8 days ago&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slice&lt;/td&gt;
&lt;td&gt;body/en only&lt;/td&gt;
&lt;td&gt;Content drift&lt;/td&gt;
&lt;td&gt;Golden set + slice re-embed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recall after fix&lt;/td&gt;
&lt;td&gt;0.87 (recovered)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Freshness before recall&lt;/strong&gt;&lt;/strong&gt; — the cheapest cause to rule out is a stalled pipeline. Check freshness and queue depth before assuming model drift.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Trend, not point-in-time&lt;/strong&gt;&lt;/strong&gt; — a single day's recall number is noise. The 7d-vs-30d trend catches genuine regressions and ignores day-to-day variance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Slice analysis&lt;/strong&gt;&lt;/strong&gt; — grouping by tier, language, and query cluster localises the drop. A uniform drop points to model; a concentrated drop points to content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Golden-set as living document&lt;/strong&gt;&lt;/strong&gt; — every drift investigation ends with a golden-set expansion. The set gets sharper over time; the next drift is caught faster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — drift monitoring costs one API call per golden query per night (200 × $0.00002 × 30 = $0.12/month) plus one Prometheus scrape. The avoided cost of a delayed drift response — a week of degraded search — is a multi-order-of-magnitude win.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — sql&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL problems on golden query sets and recall@k trends&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;


&lt;p&gt;&lt;span&gt;Optimization&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — optimization&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Optimization problems on drift alerts and freshness SLA&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/optimization" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;





&lt;h2&gt;
  
  
  Cheat sheet — embeddings refresh recipes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hash-based change-queue schema.&lt;/strong&gt; &lt;code&gt;documents(doc_id PK, content, sha256_hash, embedding vector(D), model_tag, updated_at, embedded_at, embed_attempts)&lt;/code&gt;. A BEFORE INSERT/UPDATE trigger keeps &lt;code&gt;sha256_hash&lt;/code&gt; in sync. A partial index on &lt;code&gt;(updated_at) WHERE embedded_at IS NULL OR embedded_at &amp;lt; updated_at&lt;/code&gt; keeps the queue scan O(queue_depth). The &lt;code&gt;embed_queue&lt;/code&gt; view selects the changed rows; the worker takes &lt;code&gt;LIMIT 256 FOR UPDATE SKIP LOCKED&lt;/code&gt; and upserts. Poison-pill guard: exclude rows where &lt;code&gt;embed_attempts &amp;gt;= 3&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;OpenAI batch embed call.&lt;/strong&gt; Build a JSONL file with one &lt;code&gt;{"custom_id": doc_id, "method": "POST", "url": "/v1/embeddings", "body": {"model": "text-embedding-3-small", "input": text}}&lt;/code&gt; per row. &lt;code&gt;openai.files.create(purpose="batch")&lt;/code&gt; → &lt;code&gt;openai.batches.create(endpoint="/v1/embeddings", completion_window="24h")&lt;/code&gt;. Poll &lt;code&gt;openai.batches.retrieve(batch_id).status&lt;/code&gt; until &lt;code&gt;completed&lt;/code&gt;. Batch tier is unconditionally 50% off; use it whenever freshness SLA &amp;gt; 24 h.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cost estimator template.&lt;/strong&gt; &lt;code&gt;monthly_cost = docs × avg_tokens_per_doc × runs_per_month × price_per_1k / 1000 × tier_multiplier&lt;/code&gt;. Plug in $0.00002/1K for &lt;code&gt;text-embedding-3-small&lt;/code&gt;, $0.00013/1K for &lt;code&gt;text-embedding-3-large&lt;/code&gt;, 0.5 for batch tier. For self-hosted: &lt;code&gt;gpu_hours × gpu_hourly&lt;/code&gt; where &lt;code&gt;gpu_hours = tokens / (tokens_per_sec × 3600)&lt;/code&gt; — BGE-large on L4 does ~1000 tokens/sec. Realistic self-host TCO also includes ~0.25 FTE ($6K/month) plus amortised setup ($1250/month for 3 senior-engineer-weeks over 12 months).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Model version column pattern.&lt;/strong&gt; Every row stores &lt;code&gt;model_tag = '&amp;lt;model_name&amp;gt;@&amp;lt;version&amp;gt;'&lt;/code&gt; (e.g. &lt;code&gt;text-embedding-3-small@v2&lt;/code&gt;). Retrieval SQL always filters &lt;code&gt;WHERE model_tag = current_model&lt;/code&gt; — mixed-model retrieval is meaningless. Migrations are shadow-column (ADD &lt;code&gt;embedding_v2 vector(new_dim)&lt;/code&gt;) → backfill (batch API) → dual-write for 1–2 weeks → cutover via transactional RENAME → 4-week rollback window → DROP old column. Feature-flag-gated read path enables 60-second rollback.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Recall@k drift monitor query.&lt;/strong&gt; &lt;code&gt;golden_queries(query_id, query_text, relevant_ids INT[], corpus_tier, language)&lt;/code&gt; stores the 100–500 curated pairs. Nightly runner embeds each &lt;code&gt;query_text&lt;/code&gt;, runs &lt;code&gt;SELECT doc_id FROM documents WHERE model_tag = current_model ORDER BY embedding &amp;lt;=&amp;gt; $1 LIMIT 10&lt;/code&gt;, computes &lt;code&gt;recall = |retrieved ∩ relevant| / |relevant|&lt;/code&gt; and MRR, writes to &lt;code&gt;recall_at_k_history(model_tag, corpus_tier, recall, mrr, ran_at)&lt;/code&gt;. Alert: trailing 7-day avg &amp;lt; 30-day avg × 0.95.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Freshness SLA per tier.&lt;/strong&gt; Head tier (titles / prices / stock): 5 minutes. Body tier (descriptions / docs): 4 hours. Long-tail (archived): 24 hours. Alert on &amp;gt; 5% row-count past SLA — &lt;code&gt;SELECT corpus_tier, COUNT(*) FILTER (WHERE embedded_at &amp;lt; now() - tier_sla) * 100.0 / COUNT(*) FROM documents GROUP BY corpus_tier&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Three-tier refresh cadence.&lt;/strong&gt; Streaming (CDC → Kafka → real-time API) for head tier; hourly cron + real-time API for body tier; nightly batch API for long-tail. Per-field &lt;code&gt;embed_stream_at&lt;/code&gt; / &lt;code&gt;embed_desc_at&lt;/code&gt; / &lt;code&gt;embed_tax_at&lt;/code&gt; cursors so a change to one field re-embeds only that tier's vector.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Self-host crossover.&lt;/strong&gt; OpenAI &lt;code&gt;text-embedding-3-small&lt;/code&gt; batch tier vs self-hosted BGE-large-en-v1.5 on L4 spot. Realistic crossover is ~3B tokens/month once 0.25 FTE ops + amortised setup are included. Under that scale, managed API wins on TCO. Over that scale (and with an existing GPU-ops team), self-host wins by 3–5×.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dual-write during migration.&lt;/strong&gt; Every incoming write embeds against both &lt;code&gt;old_model&lt;/code&gt; and &lt;code&gt;new_model&lt;/code&gt;, populates both &lt;code&gt;embedding&lt;/code&gt; and &lt;code&gt;embedding_v2&lt;/code&gt;. Duration: 1–2 weeks — long enough for the recall@k monitor to compare on production traffic. Cost overhead: 2× normal write cost for the window. Cutover: transactional RENAME to swap columns. Rollback: feature flag flips read path back to &lt;code&gt;embedding_old&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;GDPR erasure via tombstone.&lt;/strong&gt; &lt;code&gt;deleted_at TIMESTAMPTZ&lt;/code&gt; column; app writes &lt;code&gt;UPDATE documents SET deleted_at = now()&lt;/code&gt; on erasure request. &lt;code&gt;embed_delete_queue&lt;/code&gt; view selects &lt;code&gt;WHERE deleted_at IS NOT NULL AND embedding IS NOT NULL&lt;/code&gt;. Sync job NULLs the &lt;code&gt;embedding&lt;/code&gt; column and calls the vector-store delete API. &lt;code&gt;deleted_at&lt;/code&gt; stays populated forever as audit trail; &lt;code&gt;embedding IS NULL&lt;/code&gt; is the "processed" flag.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Poison-pill dead letter.&lt;/strong&gt; Rows that fail 3 embed attempts route to &lt;code&gt;embed_dead_letter(doc_id, last_error, attempts, first_failed_at, last_failed_at)&lt;/code&gt;. Nightly triage: reasons include oversized content (chunk further), content-policy violations (skip + flag), provider bug (retry after provider fix), corrupted UTF-8 (repair pipeline). Never leave the queue jammed on one poison pill.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Query-side model consistency.&lt;/strong&gt; The query is embedded with the &lt;em&gt;same&lt;/em&gt; model as the corpus. &lt;code&gt;retrieve(q_text)&lt;/code&gt; reads &lt;code&gt;current_model&lt;/code&gt; from config, embeds &lt;code&gt;q_text&lt;/code&gt; with it, and filters &lt;code&gt;WHERE model_tag = current_model&lt;/code&gt; in the SQL. Any mismatch — old query model against new corpus model, or vice versa — returns semantically arbitrary results.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Batch size for the API.&lt;/strong&gt; OpenAI accepts up to 2048 inputs per embeddings request but the per-request throughput drops past ~256. Batch = 256 is the sweet spot: high enough for the network overhead to amortise, low enough for retries on partial failures to be cheap.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Vector store sync.&lt;/strong&gt; pgvector — same table, one UPDATE. Pinecone — &lt;code&gt;index.upsert([(id, vec, metadata)])&lt;/code&gt;. Weaviate — &lt;code&gt;client.batch.add_data_object(...)&lt;/code&gt;. All three support batch mode; use it. Always include &lt;code&gt;model_tag&lt;/code&gt; as metadata so cross-model filtering works at the vector-store layer, not just the SQL layer.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Do I need to re-embed my corpus periodically?
&lt;/h3&gt;

&lt;p&gt;Yes, but only the rows that changed. Full-corpus re-embed nightly is the anti-pattern that runs up a 5-figure OpenAI bill for zero benefit. The right pattern is a content-hash + &lt;code&gt;embedded_at&lt;/code&gt; cursor: &lt;code&gt;WHERE embedded_at IS NULL OR embedded_at &amp;lt; updated_at&lt;/code&gt;. This turns a linear-in-corpus job into a linear-in-churn job — typically a 50× cost reduction with identical freshness. Full-corpus re-embed is only justified when (a) the embedding model itself changes (a migration event), (b) the content pre-processing pipeline changes (new chunking, new normalisation), or (c) as a periodic freshness reset every ~6 months to purge any accumulated inconsistencies. Otherwise, the incremental hash-driven refresh is the whole story.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I detect changed documents?
&lt;/h3&gt;

&lt;p&gt;Three signals, in ascending order of freshness and complexity. &lt;strong&gt;Content hash + &lt;code&gt;updated_at&lt;/code&gt;&lt;/strong&gt; — the cheapest option, works with a plain Postgres cron job. A BEFORE INSERT/UPDATE trigger computes &lt;code&gt;sha256_hash = digest(content, 'sha256')&lt;/code&gt;, and the embed worker selects &lt;code&gt;WHERE embedded_at &amp;lt; updated_at AND sha256_hash != last_embedded_hash&lt;/code&gt;. &lt;strong&gt;CDC via Debezium&lt;/strong&gt; — Postgres logical decoding streams every row change to Kafka; a consumer batches and embeds. Sub-second freshness; scales horizontally. &lt;strong&gt;ETL delta table&lt;/strong&gt; — for stacks already on dbt or Airflow, a nightly &lt;code&gt;changed_docs&lt;/code&gt; model provides the queue. Choose based on freshness SLA and existing infrastructure: hash + cron under 1M change events/day; CDC over that or when sub-minute freshness matters.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does an embedding refresh cost?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;monthly_cost = tokens × price_per_1k / 1000 × tier_multiplier&lt;/code&gt;. On OpenAI &lt;code&gt;text-embedding-3-small&lt;/code&gt; at $0.00002/1K tokens, batch tier (50% off), a 5M-doc corpus with 400 tokens/doc and a 2% daily churn costs about $12/month. The same corpus on nightly full re-embed at real-time pricing is $600/month — a 50× cost delta from switching to incremental. Model choice adds another 6.5× spread: &lt;code&gt;text-embedding-3-large&lt;/code&gt; at $0.00013/1K is 6.5× more expensive than &lt;code&gt;text-embedding-3-small&lt;/code&gt; for a 5–10% recall gain. The right defaults: &lt;code&gt;text-embedding-3-small&lt;/code&gt; on batch tier for the base cost, incremental change detection for the primary cost lever, and per-field tiered cadence for the last 30% of savings. Self-hosted BGE/E5 only breaks even at ~3B tokens/month once realistic TCO (GPU + 0.25 FTE + amortised setup) is included.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I self-host embedding models?
&lt;/h3&gt;

&lt;p&gt;Only past ~3B tokens/month, or when API rate limits become a real bottleneck. The paper cost of self-hosting BGE-large on an L4 spot GPU is ~$0.00000005 per token — a fraction of OpenAI's &lt;code&gt;text-embedding-3-small&lt;/code&gt; batch price. But realistic TCO includes ~0.25 FTE for on-call, model updates, and GPU orchestration (~$6K/month), plus amortised setup of ~3 senior-engineer-weeks. That is ~$7,000/month floor for self-hosting &lt;em&gt;anything&lt;/em&gt;. Under 3B tokens/month, OpenAI batch is cheaper on TCO. Over that scale — a 50M-doc corpus refreshed weekly, or a high-throughput real-time inference workload — self-hosting starts to make real financial sense. The other reason to self-host is data-locality: some regulated industries (healthcare, defence) forbid sending customer content to external APIs, in which case the TCO conversation is moot.&lt;/p&gt;

&lt;h3&gt;
  
  
  When do I upgrade to a new embedding model?
&lt;/h3&gt;

&lt;p&gt;Trigger on one of three signals. &lt;strong&gt;Recall@k improvement on the golden set.&lt;/strong&gt; Benchmark the new model against the current on the same golden set; if recall@10 improves by 3+ points, the migration is likely worth it. &lt;strong&gt;Cost reduction.&lt;/strong&gt; If a new model is materially cheaper for equivalent quality (e.g. &lt;code&gt;text-embedding-3-small&lt;/code&gt; was cheaper than &lt;code&gt;ada-002&lt;/code&gt; at launch), migrate. &lt;strong&gt;Deprecation.&lt;/strong&gt; Providers eventually deprecate old models; migrate before the deprecation window closes. The migration pattern is always the same four steps: shadow column → backfill on batch tier → dual-write validation window → transactional cutover, with a 4-week rollback window via column preservation and a feature-flag-gated read path. Total migration cost for a 20M-doc corpus is roughly $500 on batch tier — a small fraction of the recall or cost win that motivated the migration.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I monitor embedding drift?
&lt;/h3&gt;

&lt;p&gt;A golden query set + a nightly recall@k trend. Curate 100–500 hand-labelled &lt;code&gt;(query_text, relevant_doc_ids)&lt;/code&gt; pairs covering head queries, long tail, and adversarial cases. A nightly job embeds each query with the current model, retrieves the top-10 from pgvector filtered by &lt;code&gt;model_tag = current_model&lt;/code&gt;, computes recall@10 and MRR against the labelled relevants, and writes the result to a &lt;code&gt;recall_at_k_history&lt;/code&gt; table. Alert on &lt;code&gt;trailing_7d_avg &amp;lt; trailing_30d_avg × 0.95&lt;/code&gt; — a 5% relative drop that survives day-to-day noise. Group the metric by &lt;code&gt;corpus_tier&lt;/code&gt; and &lt;code&gt;language&lt;/code&gt; so a drop can be localised to a slice. Pair the recall monitor with a freshness monitor — &lt;code&gt;now() - embedded_at &amp;gt; tier_sla&lt;/code&gt; — to catch the case where the pipeline stalled instead of the vectors degraded. Together, freshness + recall cover the whole failure surface: pipeline-not-running and pipeline-running-but-degrading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practice on PipeCode
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Drill the &lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;SQL practice library →&lt;/a&gt; for the change-queue, hash-diff, and model-tag-filter problems senior interviewers love.&lt;/li&gt;
&lt;li&gt;Rehearse on the &lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;ETL practice library →&lt;/a&gt; for the incremental refresh, CDC-triggered, and tiered-cadence pipelines that keep the OpenAI bill under control.&lt;/li&gt;
&lt;li&gt;Sharpen the tuning axis with the &lt;a href="https://pipecode.ai/explore/practice/topic/optimization" rel="noopener noreferrer"&gt;optimization practice library →&lt;/a&gt; for the cost-tier, batch-sizing, and drift-alert problems.&lt;/li&gt;
&lt;li&gt;Stack the prerequisites against PipeCode's broader 450+ data-engineering catalogue to anchor the four-axis intuition (incremental, cost, versioning, drift) against real graded inputs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Lock in embeddings-refresh muscle memory&lt;/h3&gt;

&lt;p&gt;Provider docs explain the API. PipeCode drills explain the decision — when to switch from nightly full to incremental, when to move to batch tier, when to migrate to a new model, when to alert on recall@k drift. &lt;a href="https://pipecode.ai/" rel="noopener noreferrer"&gt;Pipecode.ai&lt;/a&gt; is Leetcode for Data Engineering — pattern-first practice tuned for the production trade-offs senior data engineers actually face.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;Practice SQL problems →&lt;/a&gt;&lt;br&gt;
&lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;Practice ETL problems →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>sql</category>
      <category>interview</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Online + Offline Feature Store Sync: Freshness Budgets &amp; Backfill Strategies</title>
      <dc:creator>Gowtham Potureddi</dc:creator>
      <pubDate>Fri, 10 Jul 2026 16:24:25 +0000</pubDate>
      <link>https://dev.to/gowthampotureddi/online-offline-feature-store-sync-freshness-budgets-backfill-strategies-522</link>
      <guid>https://dev.to/gowthampotureddi/online-offline-feature-store-sync-freshness-budgets-backfill-strategies-522</guid>
      <description>&lt;p&gt;&lt;strong&gt;&lt;code&gt;online feature store&lt;/code&gt;&lt;/strong&gt; is the sub-millisecond key-value tier that every serious ML serving path leans on — and the component senior interviewers probe hardest because it is where &lt;em&gt;training data&lt;/em&gt; and &lt;em&gt;serving data&lt;/em&gt; diverge, silently, until a model score starts drifting in production. The offline store (a warehouse or lake table partitioned by &lt;code&gt;event_ts&lt;/code&gt;) is the source of truth for training and point-in-time (PIT) joins; the &lt;code&gt;online feature store&lt;/code&gt; is a bounded Redis, DynamoDB, or Cassandra keyspace that the serving path can hit in single-digit milliseconds. &lt;code&gt;feature store sync&lt;/code&gt; is the contract between the two — the pipeline that keeps them coherent — and the engineering trade-off lives in &lt;em&gt;how often you sync&lt;/em&gt;, &lt;em&gt;which paths you use&lt;/em&gt;, and &lt;em&gt;what freshness budget you promise per feature&lt;/em&gt;, not in whether you need both stores at all.&lt;/p&gt;

&lt;p&gt;This guide is the senior-DE walkthrough you wished existed the first time an interviewer asked "explain the difference between materialization and streaming push in a &lt;code&gt;feast online store&lt;/code&gt;" or "walk me through a &lt;code&gt;feature backfill&lt;/code&gt; that has to hydrate 90 days of history into &lt;code&gt;redis feature store&lt;/code&gt; rows without overloading the primary" or "what does &lt;code&gt;feature freshness&lt;/code&gt; mean for a per-second-scored fraud model versus a daily-scored churn model?" It walks through why the two stores are architecturally distinct, the three online-store backends (Redis, DynamoDB, Cassandra/Scylla) and when each wins, the warehouse-vs-lake offline choice with PIT-join semantics, the materialization + streaming push sync patterns with idempotent upserts, the per-feature &lt;code&gt;feature ttl&lt;/code&gt; and &lt;code&gt;feature freshness&lt;/code&gt; SLA table, and the backfill wave that hydrates historic online rows without saturating anything. Each section pairs a teaching block with a Solution-Tail interview answer — code, a step-by-step trace, an output table, then a concept-by-concept breakdown of why it works.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo8w0ai8ookv7tf7vci18.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo8w0ai8ookv7tf7vci18.jpeg" alt="PipeCode blog header for online + offline feature store sync — bold white headline 'Feature Store Sync' with subtitle 'Online + Offline · Freshness · Backfill' and a hero composition of two cylinders connected by a purple sync seal on a dark gradient with purple, green, orange, and blue accents and a small pipecode.ai attribution." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you want &lt;strong&gt;hands-on reps&lt;/strong&gt; immediately after reading, drill the &lt;a href="https://pipecode.ai/explore/practice/topic/streaming" rel="noopener noreferrer"&gt;streaming practice library →&lt;/a&gt;, rehearse on the &lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;ETL practice library →&lt;/a&gt;, and sharpen the tuning axis with the &lt;a href="https://pipecode.ai/explore/practice/topic/optimization" rel="noopener noreferrer"&gt;optimization practice library →&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;On this page&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why two feature stores are one Rube-Goldberg machine unless you plan them together&lt;/li&gt;
&lt;li&gt;Online stores — Redis, DynamoDB, KV backend&lt;/li&gt;
&lt;li&gt;Offline stores — warehouse or lake&lt;/li&gt;
&lt;li&gt;Sync — materialization + streaming push&lt;/li&gt;
&lt;li&gt;Freshness budgets + backfill&lt;/li&gt;
&lt;li&gt;Cheat sheet — feature store sync recipes&lt;/li&gt;
&lt;li&gt;Frequently asked questions&lt;/li&gt;
&lt;li&gt;Practice on PipeCode&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1. Why two feature stores are one Rube-Goldberg machine unless you plan them together
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Two stores, four axes — sync, freshness, backfill, consistency — decide every design in the interview
&lt;/h3&gt;

&lt;p&gt;The one-sentence invariant: &lt;strong&gt;an ML platform has an offline feature store (a warehouse or lake table for training and PIT joins) and an &lt;code&gt;online feature store&lt;/code&gt; (a bounded KV tier for serving), and everything an interviewer probes reduces to how you keep the two coherent along four axes — sync frequency, freshness budget per feature, backfill scope, and read/write consistency&lt;/strong&gt;. Treat them as one product with two surfaces; the moment they diverge without instrumentation, the training/serving skew that pollutes every model score in production has begun.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four axes interviewers actually probe.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sync.&lt;/strong&gt; How data flows from the offline store into the &lt;code&gt;online feature store&lt;/code&gt;. The options are batch materialization (nightly, hourly, on-demand), streaming push (Kafka → transform → upsert into online), or dual-write from the source event stream directly. Sync is where cost, latency, and correctness collide.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Freshness.&lt;/strong&gt; The maximum staleness a feature can carry before it is unfit for serving. &lt;code&gt;feature freshness&lt;/code&gt; is a per-feature SLA (1 minute for a user-session tally, 15 minutes for a rolling seven-day aggregate, 1 hour for a batch demographic score). The senior signal is naming the SLA per feature, not one number for the platform.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backfill.&lt;/strong&gt; How you populate the online store with historic feature values when a new feature ships or when the online store has been cold-started. &lt;code&gt;feature backfill&lt;/code&gt; is where correctness gets tested — the backfill must produce the same value the online path would have produced at each &lt;code&gt;event_ts&lt;/code&gt;, or the training/serving contract breaks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistency.&lt;/strong&gt; Whether the online store returns strictly the most recent write (read-your-write) or an eventually consistent snapshot. For serving, eventual consistency with a bounded staleness window is usually enough; for compliance features (KYC, entitlement) you need strong consistency and a stricter SLA.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why "just use one store" is the wrong answer.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Training workload.&lt;/strong&gt; Trainers want &lt;em&gt;large scans&lt;/em&gt; — every row for the last two years, joined with labels, joined again with other features via PIT. That is a warehouse or a lake table, columnar, partitioned by &lt;code&gt;event_ts&lt;/code&gt;, cheap per byte.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Serving workload.&lt;/strong&gt; Servers want &lt;em&gt;point lookups&lt;/em&gt; — one entity, right now, sub-millisecond. That is a KV store, in-memory, indexed by entity key, cheap per read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Impedance mismatch.&lt;/strong&gt; No single storage system is good at both. Warehouses take seconds to answer a point lookup; KV stores collapse under a full-table scan. Trying to serve from Snowflake or train from Redis is the first architectural mistake juniors make when asked to "simplify" the stack.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost story.&lt;/strong&gt; The offline store pays cents per GB per month; the online store pays cents per GB per &lt;em&gt;hour&lt;/em&gt;. Keeping only the hot working set in the online tier — and everything historic in the offline tier — is where the cost model actually works.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The 2026 reality — every managed feature-store product models the split.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Feast.&lt;/strong&gt; Explicitly separates &lt;code&gt;offline_store&lt;/code&gt; and &lt;code&gt;online_store&lt;/code&gt; in the &lt;code&gt;feature_store.yaml&lt;/code&gt;. Materialization command bridges the two.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tecton.&lt;/strong&gt; Batch, streaming, and on-demand feature views; the platform manages the offline (S3 + Snowflake) and online (DynamoDB) tiers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Databricks Feature Store.&lt;/strong&gt; Online tables push from Delta into a DynamoDB or Cosmos DB online view; the PIT join stays on the Delta side.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snowflake Feature Store.&lt;/strong&gt; Offline in Snowflake; online is a serverless hot view backed by a managed KV.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sagemaker Feature Store.&lt;/strong&gt; Offline in S3 (Iceberg), online in a managed key-value tier — the two are marketed as one product but are two systems under the covers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What interviewers listen for.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do you say &lt;strong&gt;"one product with two surfaces"&lt;/strong&gt; in the first sentence? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you name &lt;strong&gt;all four axes — sync, freshness, backfill, consistency —&lt;/strong&gt; without prompting? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you push back on &lt;strong&gt;"just serve from the warehouse"&lt;/strong&gt; with a p99-latency argument? — required answer.&lt;/li&gt;
&lt;li&gt;Do you describe &lt;strong&gt;per-feature freshness SLAs&lt;/strong&gt; (not a single platform SLA)? — senior signal.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Worked example — one store fails both workloads
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The textbook anti-pattern: a startup ML team ships a rider-eta feature by writing rows into Snowflake and querying Snowflake from the serving path. It works for a week; then the CTR-optimising client starts hitting a p99 latency of 900 ms per prediction because each score triggers a warm-warehouse round trip. The on-call fix is to cache Snowflake responses in Redis, but the cache has no TTL story, no backfill story, and no freshness contract — so the second bug is that yesterday's &lt;code&gt;rider_eta&lt;/code&gt; starts serving on stale rides. Walk an interviewer through what actually happened and the two-store architecture that fixes it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The symptom (latency).&lt;/strong&gt; p99 prediction latency = 900 ms because the serving path does &lt;code&gt;SELECT * FROM feature_table WHERE rider_id = ?&lt;/code&gt; against Snowflake.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The naive fix.&lt;/strong&gt; Wrap Snowflake in an ad-hoc Redis cache with no TTL policy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The second bug (staleness).&lt;/strong&gt; The Redis entry never expires; a stale &lt;code&gt;rider_eta&lt;/code&gt; from yesterday is served to today's request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The real architecture.&lt;/strong&gt; Two stores, one contract — warehouse for training + PIT + backfill, KV for serving, and a materialization job that upserts the online store on a defined schedule with a defined TTL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; An ML platform team scores 5000 predictions per second, needs p99 serving latency &amp;lt; 20 ms, and trains against 90 days of history joined via PIT. They currently write features into Snowflake only. Design the two-store architecture, quantify the online-store size, and name the sync path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Predictions per second&lt;/td&gt;
&lt;td&gt;5000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p99 serving latency SLA&lt;/td&gt;
&lt;td&gt;&amp;lt; 20 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Training history window&lt;/td&gt;
&lt;td&gt;90 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Distinct entities (riders)&lt;/td&gt;
&lt;td&gt;20 million&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feature payload per entity&lt;/td&gt;
&lt;td&gt;2 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Freshness SLA per feature&lt;/td&gt;
&lt;td&gt;5 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# feature_store.yaml — Feast dual-store setup&lt;/span&gt;
&lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rideshare_platform&lt;/span&gt;
&lt;span class="na"&gt;registry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;s3://rideshare-fs/registry.pb&lt;/span&gt;
&lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws&lt;/span&gt;

&lt;span class="na"&gt;offline_store&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;snowflake.offline&lt;/span&gt;
  &lt;span class="na"&gt;account&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rideshare.us-east-1&lt;/span&gt;
  &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;FEATURE_STORE&lt;/span&gt;
  &lt;span class="na"&gt;warehouse&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;FS_WH&lt;/span&gt;
  &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PUBLIC&lt;/span&gt;

&lt;span class="na"&gt;online_store&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dynamodb&lt;/span&gt;
  &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-east-1&lt;/span&gt;
  &lt;span class="na"&gt;table_name_template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rideshare_{project}_{name}"&lt;/span&gt;

&lt;span class="na"&gt;entity_key_serialization_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Sizing the online store
&lt;/span&gt;&lt;span class="n"&gt;distinct_entities&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20_000_000&lt;/span&gt;
&lt;span class="n"&gt;payload_per_entity&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;          &lt;span class="c1"&gt;# bytes
&lt;/span&gt;&lt;span class="n"&gt;online_size_gb&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;distinct_entities&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;payload_per_entity&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;online store hot working set = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;online_size_gb&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; GB&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# online store hot working set = 38.1 GB
&lt;/span&gt;
&lt;span class="c1"&gt;# Cost math — DynamoDB on-demand
&lt;/span&gt;&lt;span class="n"&gt;gb_month_cost&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.25&lt;/span&gt;       &lt;span class="c1"&gt;# $ per GB-month
&lt;/span&gt;&lt;span class="n"&gt;reads_per_month&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
&lt;span class="n"&gt;read_cost&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reads_per_month&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.25e-7&lt;/span&gt;      &lt;span class="c1"&gt;# eventual reads
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;storage $/mo = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;online_size_gb&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;gb_month_cost&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reads   $/mo = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;read_cost&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;feature_store.yaml&lt;/code&gt; names Snowflake as the offline store (bulk scans, PIT joins, backfill origin) and DynamoDB as the online store (point-lookup serving). Feast's registry sits in S3; it is the metadata plane and does not participate in the data plane.&lt;/li&gt;
&lt;li&gt;The online-store size math: 20 million entities × 2 KB payload = 38.1 GB of hot working set. That is a comfortable single DynamoDB table; the same 38 GB in Snowflake would still be cheap for storage but expensive per read.&lt;/li&gt;
&lt;li&gt;The serving path (Feast SDK or a thin gateway) reads from DynamoDB with a &lt;code&gt;GetItem&lt;/code&gt; per entity. DynamoDB's median latency for a warm partition is 4-6 ms; the p99 is around 12 ms — well inside the 20 ms SLA.&lt;/li&gt;
&lt;li&gt;The training path reads from Snowflake with a windowed &lt;code&gt;SELECT ... WHERE event_ts BETWEEN ... AND ...&lt;/code&gt;, joined by &lt;code&gt;event_ts&lt;/code&gt; (the PIT anchor) to the label table.&lt;/li&gt;
&lt;li&gt;The materialization job (see H2 4) bridges the two — every 5 minutes it materializes the latest features from Snowflake into DynamoDB, keeping the online tier within the 5-minute freshness SLA.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Store&lt;/th&gt;
&lt;th&gt;Latency&lt;/th&gt;
&lt;th&gt;Cost per read&lt;/th&gt;
&lt;th&gt;Fits SLA&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Serving (5000 rps)&lt;/td&gt;
&lt;td&gt;DynamoDB&lt;/td&gt;
&lt;td&gt;p99 12 ms&lt;/td&gt;
&lt;td&gt;1.25e-7 $&lt;/td&gt;
&lt;td&gt;yes (&amp;lt; 20 ms)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Training (90-day scan)&lt;/td&gt;
&lt;td&gt;Snowflake&lt;/td&gt;
&lt;td&gt;seconds&lt;/td&gt;
&lt;td&gt;negligible per row&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serving from Snowflake&lt;/td&gt;
&lt;td&gt;(anti-pattern)&lt;/td&gt;
&lt;td&gt;p99 900 ms&lt;/td&gt;
&lt;td&gt;high per read&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Training from DynamoDB&lt;/td&gt;
&lt;td&gt;(anti-pattern)&lt;/td&gt;
&lt;td&gt;timeout&lt;/td&gt;
&lt;td&gt;prohibitive&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Never serve from a warehouse and never train from a KV store. Split the workload; sync the two with an explicit contract. The materialization schedule &lt;em&gt;is&lt;/em&gt; the freshness SLA.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the four axes on a single request
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Another classic: an interviewer asks the candidate to trace a single production request end-to-end. The senior answer threads &lt;em&gt;all four axes&lt;/em&gt; through the trace — the sync path that populated the row, the freshness stamp on it, the backfill history for the entity, and the consistency model of the lookup. Juniors trace the read only; seniors trace the write path that produced the row too.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The request.&lt;/strong&gt; Score a rider's ETA at &lt;code&gt;now = 2026-07-06 15:04:23 UTC&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The four axes.&lt;/strong&gt; Sync (how the row got there), freshness (how stale it is), backfill (whether the entity has full history), consistency (whether we read the most recent write).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Walk through the trace for a single prediction request, naming which pipeline populated each field and which SLA applies. Include the freshness check and the fallback if the row is missing or stale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Origin&lt;/th&gt;
&lt;th&gt;Sync path&lt;/th&gt;
&lt;th&gt;Freshness SLA&lt;/th&gt;
&lt;th&gt;Backfill&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;rider_session_length_sec&lt;/td&gt;
&lt;td&gt;Kafka click stream&lt;/td&gt;
&lt;td&gt;streaming push&lt;/td&gt;
&lt;td&gt;60 s&lt;/td&gt;
&lt;td&gt;30 d&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rider_7d_ride_count&lt;/td&gt;
&lt;td&gt;Snowflake aggregate&lt;/td&gt;
&lt;td&gt;batch materialize&lt;/td&gt;
&lt;td&gt;15 min&lt;/td&gt;
&lt;td&gt;90 d&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rider_lifetime_score&lt;/td&gt;
&lt;td&gt;Snowflake ML model output&lt;/td&gt;
&lt;td&gt;daily materialize&lt;/td&gt;
&lt;td&gt;24 h&lt;/td&gt;
&lt;td&gt;365 d&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rider_kyc_verified&lt;/td&gt;
&lt;td&gt;KYC service&lt;/td&gt;
&lt;td&gt;on-demand read-through&lt;/td&gt;
&lt;td&gt;strong consistency&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Serving path with per-feature freshness assertions
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;

&lt;span class="n"&gt;TABLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dynamodb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nc"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rideshare_features_rider&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;NOW&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="n"&gt;FRESHNESS_SLA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_session_length_sec&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;# seconds
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_7d_ride_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_lifetime_score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_features&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rider_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TABLE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;entity_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rider_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;row&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Item&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;LookupError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cold entity rider_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rider_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;; falling back to defaults&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;checked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sla&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;FRESHNESS_SLA&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;value_ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;age_s&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;value_ts&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;age_s&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;sla&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Emit staleness metric; caller can fall back or degrade gracefully
&lt;/span&gt;            &lt;span class="nf"&gt;emit_stale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;age_s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sla&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;_default&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="c1"&gt;# kyc_verified is read-through — strong-consistency, from a live service
&lt;/span&gt;    &lt;span class="n"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_kyc_verified&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;kyc_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rider_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;checked&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;get_features&lt;/code&gt; opens a single &lt;code&gt;GetItem&lt;/code&gt; against DynamoDB for the entity &lt;code&gt;rider_42&lt;/code&gt;. Under the covers this is one partition-key lookup; a warm partition returns in 4-6 ms.&lt;/li&gt;
&lt;li&gt;For every feature, the row carries both the value and a per-feature &lt;code&gt;..._ts&lt;/code&gt; stamp — the timestamp the sync pipeline wrote it. The freshness check computes &lt;code&gt;age_s = now - value_ts&lt;/code&gt; and compares against the per-feature SLA.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rider_session_length_sec&lt;/code&gt; (60 s SLA) is populated by a streaming push job from the Kafka click stream. Its &lt;code&gt;..._ts&lt;/code&gt; is the event time of the last click.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rider_7d_ride_count&lt;/code&gt; (15 min SLA) is populated by a batch materialization job that pulls the aggregate from Snowflake every 5 minutes. Its &lt;code&gt;..._ts&lt;/code&gt; is the materialization run time.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rider_kyc_verified&lt;/code&gt; bypasses the online store entirely — it is a read-through call to the KYC service. This is the "strong consistency" escape hatch for compliance features that cannot tolerate any staleness.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Age at read&lt;/th&gt;
&lt;th&gt;SLA&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;rider_session_length_sec&lt;/td&gt;
&lt;td&gt;12 s&lt;/td&gt;
&lt;td&gt;60 s&lt;/td&gt;
&lt;td&gt;fresh — used&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rider_7d_ride_count&lt;/td&gt;
&lt;td&gt;4 min&lt;/td&gt;
&lt;td&gt;15 min&lt;/td&gt;
&lt;td&gt;fresh — used&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rider_lifetime_score&lt;/td&gt;
&lt;td&gt;18 h&lt;/td&gt;
&lt;td&gt;24 h&lt;/td&gt;
&lt;td&gt;fresh — used&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rider_kyc_verified&lt;/td&gt;
&lt;td&gt;live&lt;/td&gt;
&lt;td&gt;strong&lt;/td&gt;
&lt;td&gt;fetched from KYC service&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Every feature carries a timestamp; every serving read compares the timestamp to the SLA. Stale features either fall back to a default, degrade the prediction gracefully, or trigger a synchronous read-through — never silently pollute the score.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — training/serving skew from one skipped axis
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The most expensive bug in ML platforms: the offline computes &lt;code&gt;count(distinct sessions in the last 7 rolling days as of row.event_ts)&lt;/code&gt; while the online computes &lt;code&gt;count(distinct sessions in the last 7 calendar days as of midnight)&lt;/code&gt;. Same feature name, two definitions — the model trains on one distribution and serves on another. The fix is to make the sync pipeline mechanically identical to the offline computation, not merely conceptually similar.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The skew.&lt;/strong&gt; Two definitions of "the same" feature; model scores drift on window-boundary days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The fix.&lt;/strong&gt; One canonical feature-view spec; both paths execute the &lt;em&gt;same&lt;/em&gt; spec.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; A team ships a &lt;code&gt;rider_7d_session_count&lt;/code&gt; feature. The offline definition uses a rolling window; the online sync uses a nightly calendar aggregation. Show the code drift, name the axis that was skipped, and propose the corrected feature-view spec.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Definition&lt;/th&gt;
&lt;th&gt;Values on a Wednesday for rider_42&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Offline (training)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;count(distinct session_id) where event_ts BETWEEN row.event_ts - 7d AND row.event_ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;41&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Online (serving)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;count(distinct session_id) where event_date BETWEEN today - 6 AND today - 1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Skew&lt;/td&gt;
&lt;td&gt;8 sessions (20%)&lt;/td&gt;
&lt;td&gt;model score drifts by ~5%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Broken — separate offline and online definitions
# Offline (Snowflake, training pipeline)
&lt;/span&gt;&lt;span class="n"&gt;OFFLINE_SQL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
SELECT row.event_ts,
       row.rider_id,
       COUNT(DISTINCT s.session_id) AS rider_7d_session_count
FROM   labelled_events row
LEFT   JOIN sessions s
       ON s.rider_id = row.rider_id
      AND s.event_ts BETWEEN row.event_ts - INTERVAL &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;7 days&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; AND row.event_ts
GROUP  BY row.event_ts, row.rider_id
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="c1"&gt;# Online (nightly materialization job)
&lt;/span&gt;&lt;span class="n"&gt;ONLINE_SQL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
SELECT rider_id,
       COUNT(DISTINCT session_id) AS rider_7d_session_count
FROM   sessions
WHERE  event_date &amp;gt;= CURRENT_DATE - 6
GROUP  BY rider_id
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="c1"&gt;# ↑ different anchor (row.event_ts vs today), different alignment (interval vs date)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Fixed — one canonical feature-view spec; both paths execute the same window
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;feast&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FeatureView&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ValueType&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;feast.types&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Int64&lt;/span&gt;

&lt;span class="n"&gt;rider_7d_session_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FeatureView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_7d_session_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;entities&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_7d_session_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Int64&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
    &lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;snowflake_batch_source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;# offline scan
&lt;/span&gt;    &lt;span class="n"&gt;online&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                         &lt;span class="c1"&gt;# sync into online store
&lt;/span&gt;    &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;window&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;7d rolling&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;anchor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Backfill and materialize both call the same aggregation UDF
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rider_7d_session_count_udf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spine_df&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sessions_df&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;spine_df&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessions_df&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;how&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;left&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INTERVAL 7 DAYS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;countDistinct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_7d_session_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The bug is a &lt;em&gt;sync skip&lt;/em&gt; — the online path does not honour the offline definition. The offline definition anchors the window on &lt;code&gt;row.event_ts&lt;/code&gt; (the PIT anchor for training); the online definition anchors on &lt;code&gt;today&lt;/code&gt; (a calendar anchor). Same feature name, two mathematical definitions.&lt;/li&gt;
&lt;li&gt;On any Wednesday, the calendar window covers Tuesday-of-last-week through Monday; the rolling window covers seven days back from &lt;code&gt;now&lt;/code&gt;. The two windows differ by up to two days of data. For a rider with bursty behaviour, the counts differ by 20% and the model score drifts by roughly 5%.&lt;/li&gt;
&lt;li&gt;The fix is a single canonical &lt;em&gt;feature-view spec&lt;/em&gt; — a Feast FeatureView (or Tecton FeatureView, or Databricks Delta Live Tables spec). The spec is the source of truth; both offline (backfill / training data generation) and online (materialize / streaming push) execute the same UDF against the same window semantics.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ttl=timedelta(days=7)&lt;/code&gt; binds the online-store TTL to the feature's semantics — after seven days, the online row is stale and should be re-materialized. This is the mechanical guarantee that online and offline stay coherent.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;tags={"window": "7d rolling", "anchor": "event_ts"}&lt;/code&gt; block is a documentation contract — anyone reading the feature-view spec knows exactly which window applies. This is the boring but essential piece that prevents the next junior from re-introducing the same skew.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Definition&lt;/th&gt;
&lt;th&gt;Training values&lt;/th&gt;
&lt;th&gt;Serving values&lt;/th&gt;
&lt;th&gt;Skew&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Broken (two SQLs)&lt;/td&gt;
&lt;td&gt;rolling from event_ts&lt;/td&gt;
&lt;td&gt;calendar from today&lt;/td&gt;
&lt;td&gt;up to 20%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixed (canonical spec)&lt;/td&gt;
&lt;td&gt;rolling from event_ts&lt;/td&gt;
&lt;td&gt;rolling from event_ts&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; A feature is defined &lt;em&gt;once&lt;/em&gt; — in the feature-view spec — and executed by both offline (backfill / training) and online (materialize / streaming push) paths. Any divergence between the two is a bug, not a feature. The Feast/Tecton/Databricks specs are not documentation; they are the operational contract.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on the two-store architecture
&lt;/h3&gt;

&lt;p&gt;A senior interviewer often opens with: "You inherit an ML platform that serves features directly from Snowflake, hits p99 latency of 800 ms on 3000 predictions per second, and has no formal freshness contract. Walk me through the two-store architecture you'd introduce, quantify the online-store size, and name the four sync/freshness/backfill/consistency decisions you'd lock down in the first week."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a Feast dual-store with per-feature SLAs and a materialization schedule
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# feature_store.yaml — the two-store shape
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
project: mlplatform
provider: aws
offline_store:
  type: snowflake.offline
  account: acme.us-east-1
  database: FEATURE_STORE
  warehouse: FS_WH
  schema: PUBLIC
online_store:
  type: dynamodb
  region: us-east-1
  table_name_template: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;acme_{project}_{name}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;
registry: s3://acme-fs/registry.pb
entity_key_serialization_version: 2
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="c1"&gt;# Feature spec — three features, three SLAs
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;feast&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FeatureView&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FeatureService&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;feast.types&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Int64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Float32&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;

&lt;span class="n"&gt;rider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Entity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ValueType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INT64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;rider_session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FeatureView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_session&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;entities&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;rider&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session_len_sec&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Int64&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
    &lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;                &lt;span class="c1"&gt;# 5-minute freshness
&lt;/span&gt;    &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;kafka_stream_source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;online&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;rider_7d_agg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FeatureView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_7d_agg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;entities&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;rider&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rides_7d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Int64&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cancel_rate_7d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Float32&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
    &lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;                  &lt;span class="c1"&gt;# 1-hour freshness
&lt;/span&gt;    &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;snowflake_batch_source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;online&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;rider_daily&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FeatureView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_daily&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;entities&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;rider&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ltv_score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Float32&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
    &lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;                 &lt;span class="c1"&gt;# 24-hour freshness
&lt;/span&gt;    &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;snowflake_batch_source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;online&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FeatureService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_scoring&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;features&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;rider_session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rider_7d_agg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rider_daily&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Sync schedule — three tiers, three cadences&lt;/span&gt;
&lt;span class="c"&gt;# 1. streaming push (session features) — Kafka consumer writes on every event&lt;/span&gt;
python push_stream.py &lt;span class="nt"&gt;--view&lt;/span&gt; rider_session &amp;amp;

&lt;span class="c"&gt;# 2. batch materialize (7d rolling aggregates) — every 5 min&lt;/span&gt;
&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; feast materialize-incremental &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; +%FT%TZ&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# 3. daily materialize (LTV scores) — nightly at 03:00 UTC&lt;/span&gt;
0 3 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; feast materialize-incremental &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; +%FT%TZ&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Origin&lt;/th&gt;
&lt;th&gt;Sync path&lt;/th&gt;
&lt;th&gt;TTL&lt;/th&gt;
&lt;th&gt;p99 online latency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;session_len_sec&lt;/td&gt;
&lt;td&gt;Kafka events&lt;/td&gt;
&lt;td&gt;streaming push&lt;/td&gt;
&lt;td&gt;5 min&lt;/td&gt;
&lt;td&gt;8 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rides_7d&lt;/td&gt;
&lt;td&gt;Snowflake window&lt;/td&gt;
&lt;td&gt;batch materialize (5 min)&lt;/td&gt;
&lt;td&gt;1 h&lt;/td&gt;
&lt;td&gt;8 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cancel_rate_7d&lt;/td&gt;
&lt;td&gt;Snowflake window&lt;/td&gt;
&lt;td&gt;batch materialize (5 min)&lt;/td&gt;
&lt;td&gt;1 h&lt;/td&gt;
&lt;td&gt;8 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ltv_score&lt;/td&gt;
&lt;td&gt;Snowflake ML batch&lt;/td&gt;
&lt;td&gt;daily materialize&lt;/td&gt;
&lt;td&gt;24 h&lt;/td&gt;
&lt;td&gt;8 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After the rollout, DynamoDB carries a 38 GB working set for 20 M riders. p99 serving latency drops from 800 ms (Snowflake) to 12 ms (DynamoDB) — 65x faster. The freshness contract is explicit per feature and monitored via a &lt;code&gt;feature_age_seconds&lt;/code&gt; gauge. Training reads directly from Snowflake with PIT joins; backfill reruns the same canonical spec.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Serving store&lt;/td&gt;
&lt;td&gt;Snowflake&lt;/td&gt;
&lt;td&gt;DynamoDB (via Feast)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p99 serving latency&lt;/td&gt;
&lt;td&gt;800 ms&lt;/td&gt;
&lt;td&gt;12 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Predictions per second&lt;/td&gt;
&lt;td&gt;3000&lt;/td&gt;
&lt;td&gt;10000+ headroom&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Freshness contract&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;per-feature SLA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backfill capability&lt;/td&gt;
&lt;td&gt;ad-hoc SQL&lt;/td&gt;
&lt;td&gt;Feast &lt;code&gt;materialize --start-date --end-date&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Offline vs online split&lt;/strong&gt;&lt;/strong&gt; — the architectural lever is &lt;em&gt;choosing the right store for each workload&lt;/em&gt;. Warehouse for scans (training, PIT joins, backfill); KV for point lookups (serving). Neither store attempts the other's workload.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Per-feature freshness SLA&lt;/strong&gt;&lt;/strong&gt; — the TTL on each FeatureView doubles as the freshness SLA. Streaming features get 5 minutes; batch aggregates get 1 hour; daily scores get 24 hours. The serving path enforces the SLA at read time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Canonical spec, two executors&lt;/strong&gt;&lt;/strong&gt; — the FeatureView is the single source of truth. Batch backfill and streaming push both execute the same spec. Training/serving skew is designed out, not caught after the fact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Sync schedule as SLA&lt;/strong&gt;&lt;/strong&gt; — the materialize cron cadence &lt;em&gt;is&lt;/em&gt; the freshness SLA. If the SLA tightens, the cron tightens; if the cron slips, the SLA is at risk. One knob, two consequences.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — DynamoDB storage for 38 GB is $10/month; on-demand reads at 5000 rps is $12/month. Snowflake compute for the materialization runs is $200/month. Total is ~$220/month — trivial compared to the p99 latency win. O(entities) storage; O(rps) read cost; O(sync_frequency) materialize cost.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Streaming&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — streaming&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Streaming problems on feature pipeline design&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/streaming" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;ETL&lt;/span&gt;
&lt;span&gt;Topic — etl&lt;/span&gt;
&lt;strong&gt;ETL problems on dual-store synchronization&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  2. Online stores — Redis, DynamoDB, KV backend
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Three online-store backends decide latency, region, and cost — pick by workload, not by fashion
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;Redis is the sub-millisecond in-memory choice for the hottest working sets, DynamoDB is the auto-scaling single-digit-ms cloud KV that never operationally embarrasses you, and Cassandra/Scylla is the multi-region high-write choice when you need active-active writes across regions&lt;/strong&gt;. Every other online-store interview question is a consequence of which backend you picked and what its TTL, replication, and payload-size limits look like.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fet9eh5ebpigqqzzlfum0.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fet9eh5ebpigqqzzlfum0.jpeg" alt="Iconographic online stores diagram — three online-store cards (Redis, DynamoDB, Cassandra) with distinct clock-glyph latency chips and TTL badges, on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Redis — the sub-millisecond default.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Latency profile.&lt;/strong&gt; Median 0.3 ms, p99 1 ms for point lookups on a warm replica. Nothing else in the ecosystem is faster for a single key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payload model.&lt;/strong&gt; String, hash, sorted set. Feature payloads are typically stored as a Redis hash keyed by &lt;code&gt;entity_id&lt;/code&gt; with one field per feature.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TTL story.&lt;/strong&gt; Per-key TTL (&lt;code&gt;EXPIRE key seconds&lt;/code&gt;) or a MAXLEN cap on lists. &lt;code&gt;feature ttl&lt;/code&gt; is enforced at the Redis layer with &lt;code&gt;EXPIREAT&lt;/code&gt; set to &lt;code&gt;event_ts + ttl_seconds&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sizing constraint.&lt;/strong&gt; In-memory — the working set must fit in RAM (times replication factor). A 100 M entity × 2 KB payload = 200 GB × 2 replicas = 400 GB RAM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When to use.&lt;/strong&gt; Sub-ms serving, hot working set fits in RAM, single-region deployment, teams with Redis operational lore.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;DynamoDB — the operationally-boring cloud KV.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Latency profile.&lt;/strong&gt; Median 4-6 ms, p99 10-12 ms for &lt;code&gt;GetItem&lt;/code&gt; on a warm partition. Auto-scales throughput without operator intervention.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payload model.&lt;/strong&gt; Item with a partition key + optional sort key + attributes. Feature rows are one item per &lt;code&gt;entity_id&lt;/code&gt; (partition key).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TTL story.&lt;/strong&gt; Per-item TTL attribute (&lt;code&gt;ttl&lt;/code&gt;) that DynamoDB expires within 48 hours of the timestamp. Not a hard SLA — for tight freshness you must materialize over stale rows rather than rely on TTL expiry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sizing constraint.&lt;/strong&gt; Payload up to 400 KB per item; unlimited items. Cost scales linearly with reads/writes and storage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When to use.&lt;/strong&gt; Managed cloud stack, single-digit-ms is fast enough, cost predictability matters, active-passive multi-region via global tables.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cassandra / Scylla — multi-region high-write.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Latency profile.&lt;/strong&gt; Median 2-4 ms, p99 8-15 ms on a well-tuned cluster. Scylla's shard-per-core design pushes p99 below Cassandra's.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payload model.&lt;/strong&gt; Row per &lt;code&gt;entity_id&lt;/code&gt; with columns per feature. Multi-region replication is native — writes replicate across data centres asynchronously.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TTL story.&lt;/strong&gt; Per-column TTL (&lt;code&gt;INSERT INTO ... USING TTL 3600&lt;/code&gt;). Compaction removes expired data during background compaction runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sizing constraint.&lt;/strong&gt; Storage is disk-backed (SSD); no RAM ceiling. Write throughput scales linearly with cluster size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When to use.&lt;/strong&gt; Active-active multi-region writes, high-write workloads (streaming push at 10k+ writes/s), teams with Cassandra ops experience.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Vector-store side-car for embedding features.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The pattern.&lt;/strong&gt; Some features are embeddings (128-dim, 512-dim vectors). Store them in the KV alongside scalar features &lt;em&gt;if&lt;/em&gt; the serving code only needs point lookups; store them in a vector store (Pinecone, Weaviate, pgvector, Vespa) if you also need approximate-nearest-neighbour search.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The dual-write.&lt;/strong&gt; Embedding features go to both the KV (for point lookup) and the vector store (for ANN). Sync them from a single Feast FeatureView with two online-store connectors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The gotcha.&lt;/strong&gt; Vector stores have their own freshness/backfill/consistency story. Do not assume the KV story transfers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on online-store choice.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Redis vs DynamoDB — walk me through the trade-off." — RAM ceiling vs auto-scale; sub-ms vs single-digit-ms.&lt;/li&gt;
&lt;li&gt;"When do you reach for Cassandra?" — active-active multi-region writes, high-write workloads.&lt;/li&gt;
&lt;li&gt;"How do you handle TTL in Redis vs DynamoDB?" — Redis is exact and immediate; DynamoDB is eventual within 48 h.&lt;/li&gt;
&lt;li&gt;"What's the payload limit in DynamoDB?" — 400 KB per item; split larger payloads or compress.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — entity → feature payload in Redis with TTL
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The canonical Redis-backed &lt;code&gt;online feature store&lt;/code&gt; write path. A materialization job takes rows from the offline store and writes them to Redis as hashes keyed by entity, with a per-key TTL that encodes the freshness SLA. The serving path does one &lt;code&gt;HGETALL&lt;/code&gt; per entity per prediction. Every design choice is a one-line answer to an interview question.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Key schema.&lt;/strong&gt; &lt;code&gt;feature_view_name:entity_id&lt;/code&gt;, e.g. &lt;code&gt;rider_session:42&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payload.&lt;/strong&gt; Redis hash with fields &lt;code&gt;session_len_sec&lt;/code&gt;, &lt;code&gt;event_ts&lt;/code&gt;, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TTL.&lt;/strong&gt; &lt;code&gt;EXPIREAT key (event_ts + ttl_seconds)&lt;/code&gt; — the row expires exactly when the freshness budget is exhausted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Design the Redis write path for a &lt;code&gt;rider_session&lt;/code&gt; feature-view with a 5-minute freshness SLA. Show the write code (materialization), the read code (serving), and the eviction semantics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature-view&lt;/th&gt;
&lt;th&gt;Entity&lt;/th&gt;
&lt;th&gt;Fields&lt;/th&gt;
&lt;th&gt;TTL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;rider_session&lt;/td&gt;
&lt;td&gt;rider_id&lt;/td&gt;
&lt;td&gt;session_len_sec, last_click_ts&lt;/td&gt;
&lt;td&gt;300 s (5 min)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Materialization writer — one HSET per entity per sync tick
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redis-fs.internal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;6379&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;decode_responses&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;write_features&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rider_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_len_sec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_session:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rider_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;pipe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mapping&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session_len_sec&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;session_len_sec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;event_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="c1"&gt;# TTL encodes the freshness SLA — key expires at event_ts + 300 s
&lt;/span&gt;    &lt;span class="n"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expireat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_ts&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Batch writer for 5000 entities per materialize tick
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;bulk_write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
    &lt;span class="n"&gt;pipe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_session:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rider_id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mapping&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session_len_sec&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session_len_sec&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="n"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expireat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Serving reader — one HGETALL per prediction
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;read_features&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rider_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_session:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rider_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hgetall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;                     &lt;span class="c1"&gt;# expired or cold
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session_len_sec&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session_len_sec&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The write path uses &lt;code&gt;HSET&lt;/code&gt; to set the hash fields and &lt;code&gt;EXPIREAT&lt;/code&gt; to set an absolute expiry timestamp on the whole hash. &lt;code&gt;EXPIREAT event_ts + 300&lt;/code&gt; means the row will vanish from Redis exactly 5 minutes after the event time — the freshness contract is a Redis primitive, not application logic.&lt;/li&gt;
&lt;li&gt;Batching via &lt;code&gt;pipe.pipeline(transaction=False)&lt;/code&gt; collapses N round-trips into one — a materialize tick of 5000 entities takes ~10 ms instead of ~2 seconds. &lt;code&gt;transaction=False&lt;/code&gt; disables &lt;code&gt;MULTI/EXEC&lt;/code&gt; because we do not need atomicity across entities; we only need throughput.&lt;/li&gt;
&lt;li&gt;The read path is a single &lt;code&gt;HGETALL&lt;/code&gt; — one round-trip, one hash fetch, returns all fields. &lt;code&gt;hgetall&lt;/code&gt; on a missing or expired key returns an empty dict, which the caller interprets as "cold entity, fall back to default."&lt;/li&gt;
&lt;li&gt;Eviction is automatic. If the materialization job stops, Redis will drop rows exactly 5 minutes after each row's &lt;code&gt;event_ts&lt;/code&gt;; the serving path immediately sees the drop and falls back rather than serving a stale row. This is superior to a TTL-less design that requires a separate garbage collector.&lt;/li&gt;
&lt;li&gt;The one caveat: &lt;code&gt;EXPIREAT&lt;/code&gt; requires a Unix timestamp; if your materialize job produces &lt;code&gt;event_ts&lt;/code&gt; in a different timezone or unit (ms vs s), the expiry math silently misfires. Convert once at the boundary and enforce the invariant with a type-checked wrapper.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Write latency per entity (pipelined)&lt;/td&gt;
&lt;td&gt;~0.002 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch write latency (5000 entities)&lt;/td&gt;
&lt;td&gt;~10 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read latency (serving)&lt;/td&gt;
&lt;td&gt;0.3 ms median, 1 ms p99&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eviction&lt;/td&gt;
&lt;td&gt;automatic at &lt;code&gt;event_ts + 300 s&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory per entity&lt;/td&gt;
&lt;td&gt;~150 bytes (small hash)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Encode the freshness SLA as &lt;code&gt;EXPIREAT&lt;/code&gt;; batch writes with a pipeline; keep the hash flat. If the payload grows beyond ~4 KB, split it into multiple hashes keyed by feature-view name — small hashes are ziplist-encoded and 3-4x more memory-efficient than large hashes.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — DynamoDB with on-demand and item TTL
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The DynamoDB flavour of the same write path. &lt;code&gt;PutItem&lt;/code&gt; per entity with a &lt;code&gt;ttl&lt;/code&gt; attribute; DynamoDB expires items within 48 hours of the timestamp. Because the expiry is eventual, the serving path still checks freshness at read time and treats expired-but-not-yet-swept items as stale.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Key model.&lt;/strong&gt; &lt;code&gt;entity_id&lt;/code&gt; as the partition key; no sort key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payload.&lt;/strong&gt; One item per entity with an attribute per feature and a &lt;code&gt;ttl&lt;/code&gt; attribute for expiry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Freshness.&lt;/strong&gt; &lt;code&gt;ttl = event_ts + budget_seconds&lt;/code&gt;; serving path also enforces &lt;code&gt;now - event_ts &amp;lt;= budget&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Design the DynamoDB write path for a &lt;code&gt;rider_7d_agg&lt;/code&gt; feature-view with a 1-hour freshness SLA. Handle the 48-hour eventual expiry with an application-level freshness check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature-view&lt;/th&gt;
&lt;th&gt;Partition key&lt;/th&gt;
&lt;th&gt;Attributes&lt;/th&gt;
&lt;th&gt;ttl attribute&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;rider_7d_agg&lt;/td&gt;
&lt;td&gt;entity_id&lt;/td&gt;
&lt;td&gt;rides_7d, cancel_rate_7d, event_ts&lt;/td&gt;
&lt;td&gt;event_ts + 3600&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="n"&gt;dynamodb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dynamodb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;table&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;acme_mlplatform_rider_7d_agg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;write_row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rider_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rides_7d&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancel_rate_7d&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Item&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;entity_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rider_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rides_7d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;rides_7d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cancel_rate_7d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancel_rate_7d&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;     &lt;span class="c1"&gt;# DDB numeric via Decimal in prod
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;event_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ttl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;             &lt;span class="n"&gt;event_ts&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;# Batch writer for a materialize tick — 25 items per BatchWriteItem
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;bulk_write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;batch_writer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Item&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;entity_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rider_id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rides_7d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rides_7d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cancel_rate_7d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cancel_rate_7d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ttl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;            &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Serving reader with application-level freshness enforcement
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;read_row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rider_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;budget_seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;entity_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rider_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;row&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Item&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;                                     &lt;span class="c1"&gt;# cold or fully expired
&lt;/span&gt;    &lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;budget_seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;emit_stale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rider_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;budget_seconds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;                                     &lt;span class="c1"&gt;# stale — caller falls back
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rides_7d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rides_7d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cancel_rate_7d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cancel_rate_7d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;PutItem&lt;/code&gt; writes the whole row in one round trip. Each item carries a &lt;code&gt;ttl&lt;/code&gt; attribute containing a Unix timestamp; DynamoDB's TTL feature expires items &lt;em&gt;within 48 hours&lt;/em&gt; of that timestamp — an eventual guarantee, not a hard one.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;batch_writer&lt;/code&gt; batches up to 25 items per &lt;code&gt;BatchWriteItem&lt;/code&gt; request under the hood. Throughput scales with the number of parallel writers; a single writer produces roughly 1000 items/s, and 20 workers produce 20k items/s comfortably.&lt;/li&gt;
&lt;li&gt;Because TTL expiry is eventual, the serving reader must not trust the presence of the item. Instead, it computes &lt;code&gt;age = now - event_ts&lt;/code&gt; and compares against the freshness budget. Stale rows are treated as if they had already expired; the caller falls back to a default or triggers a synchronous refresh.&lt;/li&gt;
&lt;li&gt;On-demand capacity (&lt;code&gt;BillingMode=PAY_PER_REQUEST&lt;/code&gt;) auto-scales without any operator intervention — the workload can spike from 100 rps to 10000 rps with no config change. The trade-off is a higher per-request cost than provisioned throughput; use provisioned for steady-state, on-demand for spiky.&lt;/li&gt;
&lt;li&gt;The single serious operational catch is the payload-size limit — 400 KB per item. Feature-view payloads that pack embeddings or long histories can exceed this; the standard fix is to compress with zstd, split into multiple items, or move the embedding to a separate table.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Write latency (single)&lt;/td&gt;
&lt;td&gt;5-8 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch write (25 items)&lt;/td&gt;
&lt;td&gt;~15 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read latency&lt;/td&gt;
&lt;td&gt;median 4-6 ms, p99 12 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTL guarantee&lt;/td&gt;
&lt;td&gt;expires within 48 h of &lt;code&gt;ttl&lt;/code&gt; timestamp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payload cap&lt;/td&gt;
&lt;td&gt;400 KB per item&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Use DynamoDB when operational simplicity matters more than the last 3 ms of latency. Trust the &lt;code&gt;ttl&lt;/code&gt; for garbage collection; never trust it for freshness — always verify &lt;code&gt;event_ts&lt;/code&gt; at read time.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — Cassandra for multi-region active-active writes
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The Cassandra flavour of the online store is the multi-region choice. A single logical keyspace replicates across three regions (us-east, us-west, eu-west) with &lt;code&gt;NetworkTopologyStrategy&lt;/code&gt;; writes at any region are asynchronously replicated to the others. This is the only mainstream KV that supports true active-active writes without a coordinator.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Topology.&lt;/strong&gt; Three data centres, replication factor 3 per DC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistency.&lt;/strong&gt; &lt;code&gt;LOCAL_QUORUM&lt;/code&gt; writes for low-latency + regional durability; &lt;code&gt;EACH_QUORUM&lt;/code&gt; for cross-region durability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TTL.&lt;/strong&gt; Per-column TTL enforced by compaction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Design the Cassandra schema for a &lt;code&gt;rider_geo_features&lt;/code&gt; feature-view that must serve reads from three regions with p99 &amp;lt; 15 ms and tolerate a full region outage without data loss.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Region&lt;/th&gt;
&lt;th&gt;RF&lt;/th&gt;
&lt;th&gt;Consistency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;us-east-1&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;LOCAL_QUORUM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;us-west-2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;LOCAL_QUORUM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;eu-west-1&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;LOCAL_QUORUM&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Keyspace with per-DC replication&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;KEYSPACE&lt;/span&gt; &lt;span class="n"&gt;feature_store&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;REPLICATION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="s1"&gt;'class'&lt;/span&gt;       &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'NetworkTopologyStrategy'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'us_east_1'&lt;/span&gt;   &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'us_west_2'&lt;/span&gt;   &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'eu_west_1'&lt;/span&gt;   &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;-- Feature-view table&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;feature_store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rider_geo&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;entity_id&lt;/span&gt;       &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;last_lat&lt;/span&gt;        &lt;span class="nb"&gt;double&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;last_lon&lt;/span&gt;        &lt;span class="nb"&gt;double&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;region&lt;/span&gt;          &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;event_ts&lt;/span&gt;        &lt;span class="nb"&gt;bigint&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Write with per-column TTL (300 s)&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;feature_store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rider_geo&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_lat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_lon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'rider_42'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;37&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;77&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;122&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'us-west-2'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1720000000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;TTL&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Read with LOCAL_QUORUM&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;last_lat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_lon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_ts&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;feature_store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rider_geo&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;entity_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'rider_42'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- ↑ driver-level consistency: LOCAL_QUORUM&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Python driver (cassandra-driver) — one prepared insert per entity
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;cassandra.cluster&lt;/span&gt;    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Cluster&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;cassandra&lt;/span&gt;            &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ConsistencyLevel&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;cassandra.query&lt;/span&gt;      &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BoundStatement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PreparedStatement&lt;/span&gt;

&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Cluster&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cassandra.us-east-1.internal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;feature_store&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;default_consistency_level&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ConsistencyLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LOCAL_QUORUM&lt;/span&gt;

&lt;span class="n"&gt;stmt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PreparedStatement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
  INSERT INTO rider_geo (entity_id, last_lat, last_lon, region, event_ts)
  VALUES (?, ?, ?, ?, ?)
  USING TTL 300
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;write_geo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rider_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;bound&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rider_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_ts&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;bound&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;consistency_level&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ConsistencyLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LOCAL_QUORUM&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bound&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;NetworkTopologyStrategy&lt;/code&gt; with per-DC replication factor 3 replicates every row to every data centre. Writes at any DC propagate asynchronously to the others; reads at LOCAL_QUORUM only wait for a quorum within the local DC (2 of 3 replicas).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;USING TTL 300&lt;/code&gt; sets a 5-minute expiry per column; Cassandra tags each cell with a tombstone timestamp and compaction removes expired data. The TTL is precise (not eventual like DynamoDB) but the compaction cadence must be tuned so tombstones do not accumulate.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LOCAL_QUORUM&lt;/code&gt; reads deliver p99 &amp;lt; 15 ms because they never cross regions — a request in us-east-1 talks to 2 of the 3 us-east replicas. Cross-DC reads (via &lt;code&gt;EACH_QUORUM&lt;/code&gt;) are rare and only used for consistency-critical audits.&lt;/li&gt;
&lt;li&gt;A regional outage is survivable — the other two DCs keep serving. The tombstone timestamps and hinted handoff mechanism reconcile the DCs on recovery.&lt;/li&gt;
&lt;li&gt;The operational cost is real: Cassandra ops (tuning compaction, monitoring hint queues, sizing GC pauses) is a full-time discipline. Scylla mitigates the Java GC pain but keeps the operational surface area. Choose Cassandra/Scylla only if you actually need multi-region active-active — for single-region deployments, DynamoDB or Redis is simpler.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Region&lt;/th&gt;
&lt;th&gt;Read latency (LOCAL_QUORUM)&lt;/th&gt;
&lt;th&gt;Data loss on regional outage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;us-east-1&lt;/td&gt;
&lt;td&gt;p99 12 ms&lt;/td&gt;
&lt;td&gt;none (2 of 2 remaining DCs)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;us-west-2&lt;/td&gt;
&lt;td&gt;p99 14 ms&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;eu-west-1&lt;/td&gt;
&lt;td&gt;p99 13 ms&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Cassandra/Scylla is the answer for active-active multi-region. Choose it when the read-latency-per-region and DC-failure-tolerance requirements together rule out global DynamoDB tables or a Redis primary/replica topology.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on picking the online-store backend
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "You're picking the online-store backend for a new ML platform. You have 50 million entities, need p99 serving latency under 15 ms, expect writes at 10k/s from a streaming push job, and must serve reads from us-east and eu-west. Walk me through the choice between Redis, DynamoDB, and Cassandra, and defend the trade-off."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using DynamoDB global tables plus a Redis sidecar for the hottest 5%
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Two-tier online store — DynamoDB global tables for durability,
# Redis for the hottest 5% (top-N by prediction rate)
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="n"&gt;dynamo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dynamodb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nc"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;acme_fs_rider_features&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redis-fs.us-east-1.internal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;6379&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;HOT_ENTITIES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_hot_set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;# top-5% by rolling prediction rate
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;write_row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rider_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;features&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ttl_seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rider_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;dynamo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Item&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;entity_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;features&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="n"&gt;event_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ttl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;event_ts&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ttl_seconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rider_id&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;HOT_ENTITIES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;pipe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;features:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mapping&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;features&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;event_ts&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="n"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expireat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;features:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_ts&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ttl_seconds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;read_row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rider_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rider_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="c1"&gt;# Try Redis first for the hot set
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rider_id&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;HOT_ENTITIES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hgetall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;features:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;
    &lt;span class="c1"&gt;# Fall back to DynamoDB
&lt;/span&gt;    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dynamo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;entity_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Item&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Redis-only&lt;/th&gt;
&lt;th&gt;DynamoDB-only&lt;/th&gt;
&lt;th&gt;Two-tier&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;p50 read latency&lt;/td&gt;
&lt;td&gt;0.3 ms&lt;/td&gt;
&lt;td&gt;5 ms&lt;/td&gt;
&lt;td&gt;0.3 ms (hot) / 5 ms (cold)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p99 read latency&lt;/td&gt;
&lt;td&gt;1 ms&lt;/td&gt;
&lt;td&gt;12 ms&lt;/td&gt;
&lt;td&gt;1 ms (hot) / 12 ms (cold)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-region writes&lt;/td&gt;
&lt;td&gt;replica-lag (async)&lt;/td&gt;
&lt;td&gt;global tables (managed)&lt;/td&gt;
&lt;td&gt;global tables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RAM required&lt;/td&gt;
&lt;td&gt;50 M × 2 KB × 2 = 200 GB&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;5 M × 2 KB × 2 = 20 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Op complexity&lt;/td&gt;
&lt;td&gt;high (sharding + failover)&lt;/td&gt;
&lt;td&gt;low (managed)&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monthly cost&lt;/td&gt;
&lt;td&gt;~$8000 (RAM)&lt;/td&gt;
&lt;td&gt;~$1500 (RCU + storage)&lt;/td&gt;
&lt;td&gt;~$2200&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;DynamoDB global tables carry the full 50 M entities and handle multi-region durability. Redis carries only the top 5% by prediction rate — the "hot set" — as an in-memory accelerator for the sub-ms p99. The write path always writes DynamoDB (source of truth) and conditionally writes Redis (cache). The read path checks Redis first for hot entities and falls through to DynamoDB for cold. p99 for hot entities is 1 ms; p99 for cold is 12 ms; overall p99 is dominated by whichever tier the tail of hot traffic lives in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Surface&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;p99 latency (hot entities)&lt;/td&gt;
&lt;td&gt;1 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p99 latency (cold entities)&lt;/td&gt;
&lt;td&gt;12 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p99 latency (blended weighted)&lt;/td&gt;
&lt;td&gt;~3 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-region durability&lt;/td&gt;
&lt;td&gt;via DynamoDB global tables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Streaming push throughput&lt;/td&gt;
&lt;td&gt;10k writes/s comfortably&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;~$2200/month (vs $8000 all-Redis or $1500 all-DDB)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Two tiers by heat&lt;/strong&gt;&lt;/strong&gt; — the hottest 5% of entities drive most of the read volume in a rideshare / fraud / recsys workload. Serving them from Redis kills the p99 for the majority of requests; DynamoDB carries the long tail and the durability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Global tables for multi-region&lt;/strong&gt;&lt;/strong&gt; — DynamoDB global tables handle cross-region replication with managed consistency semantics. No custom replication topology; the ops burden is delegated to AWS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Write-through, read-through&lt;/strong&gt;&lt;/strong&gt; — every write hits DynamoDB (source of truth) and conditionally hits Redis (accelerator). The read path prefers Redis and falls back to DynamoDB. Cache invalidation is trivial because Redis uses the same TTL as DynamoDB's &lt;code&gt;event_ts + budget&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Hot-set refresh&lt;/strong&gt;&lt;/strong&gt; — the &lt;code&gt;HOT_ENTITIES&lt;/code&gt; set is recomputed hourly from a rolling prediction-rate metric. Entities that leave the hot set stop receiving Redis writes; their Redis entries expire naturally via TTL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — 5% of entities in Redis (20 GB RAM × 2 replicas) is a rounding error compared to 100% of entities in Redis. DynamoDB is billed per read/write; the two-tier design pushes 80% of reads to Redis, which reduces DynamoDB RCU cost by a similar factor. O(hot_set) RAM; O(all_entities) DDB storage; O(rps) DDB reads.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Streaming&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — streaming&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Streaming problems on KV online-store design&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/streaming" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;Optimization&lt;/span&gt;
&lt;span&gt;Topic — optimization&lt;/span&gt;
&lt;strong&gt;Optimization problems on serving-tier latency&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/optimization" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  3. Offline stores — warehouse or lake
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Warehouse for scans, lake for portability — either way, &lt;code&gt;event_ts&lt;/code&gt; partitioning is table stakes
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;the offline feature store is either a cloud warehouse (Snowflake, BigQuery, Databricks SQL) or an open-format lake (Iceberg, Delta, Hudi on S3), partitioned by &lt;code&gt;event_ts&lt;/code&gt;, and every training + backfill + PIT-join workload runs against it&lt;/strong&gt;. Both shapes work; the choice is a cost, portability, and workload trade-off, not a correctness one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff6t8kx0efwsre1afbjwh.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff6t8kx0efwsre1afbjwh.jpeg" alt="Iconographic offline stores diagram — two large cards, warehouse (Snowflake/BigQuery/Databricks) and lake (Iceberg/Delta/Hudi), each with feature-table glyphs and a PIT-join badge, on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Warehouse — Snowflake, BigQuery, Databricks SQL.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Storage model.&lt;/strong&gt; Managed columnar (micro-partitions in Snowflake, capacitor in BigQuery, Delta in Databricks). The warehouse owns the file format.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compute model.&lt;/strong&gt; Elastic compute clusters spun up on demand; billed per second of compute plus per byte of storage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PIT join capability.&lt;/strong&gt; Native — &lt;code&gt;ASOF JOIN&lt;/code&gt; in Snowflake and Databricks, correlated subqueries with &lt;code&gt;event_ts &amp;lt;= label.event_ts&lt;/code&gt; in BigQuery.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When to use.&lt;/strong&gt; Teams that already run analytics in a warehouse; low-op-overhead offline store; strong SQL story; happy to pay the compute premium.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Lake — Iceberg, Delta, Hudi on S3.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Storage model.&lt;/strong&gt; Open Parquet files under an open table format (Iceberg, Delta, Hudi). Storage is your S3 bucket; the table format layer manages metadata, snapshots, and time-travel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compute model.&lt;/strong&gt; BYO — Spark, Trino, Flink, Athena, DuckDB, Databricks. The compute is decoupled from the storage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PIT join capability.&lt;/strong&gt; Via Spark/Trino SQL (&lt;code&gt;RANGE JOIN&lt;/code&gt; in Trino, &lt;code&gt;AsOfJoin&lt;/code&gt; in Delta). Slightly more code than the warehouse ASOF but equally correct.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When to use.&lt;/strong&gt; Multi-engine access (train with Spark, query ad-hoc with Trino), storage portability (avoid vendor lock-in), tight cost control.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;PIT (point-in-time) joins — the core offline-store primitive.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The problem.&lt;/strong&gt; For each labelled row at &lt;code&gt;event_ts&lt;/code&gt;, join the &lt;em&gt;most recent feature value known at that time&lt;/em&gt;. A naive &lt;code&gt;LEFT JOIN feature_table ON entity_id&lt;/code&gt; leaks future data into the training set — the model trains on features it would not have had at prediction time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The primitive.&lt;/strong&gt; &lt;code&gt;ASOF JOIN feature_table ON entity_id AND MATCH_CONDITION feature.event_ts &amp;lt;= row.event_ts&lt;/code&gt;. Returns the single most recent feature row per label row.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The performance concern.&lt;/strong&gt; Naive PIT joins on 100 M rows can take hours. Partition the feature table by &lt;code&gt;event_ts&lt;/code&gt;; the query planner prunes to the relevant partitions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cost trade-off — warehouse vs lake.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Storage per GB per month.&lt;/strong&gt; Warehouse: $0.02-0.04 (managed). Lake (S3): $0.023 (raw) + your ops for compaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compute per query.&lt;/strong&gt; Warehouse: $2-5 per hour of compute cluster. Lake: same for Databricks; cheaper for Athena/Trino if you already run them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Portability.&lt;/strong&gt; Lake wins — the Parquet files are yours; you can query with any engine. Warehouse locks you into the vendor's compute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ops overhead.&lt;/strong&gt; Warehouse: near-zero. Lake: significant (compaction, metadata cleanup, snapshot expiry).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on offline stores.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Warehouse vs lake for the offline store — walk me through the trade-off." — storage portability + compute cost vs ops simplicity.&lt;/li&gt;
&lt;li&gt;"How do you partition the feature table?" — by &lt;code&gt;event_ts&lt;/code&gt; (day or hour) and often by &lt;code&gt;entity_id&lt;/code&gt; bucket.&lt;/li&gt;
&lt;li&gt;"What is a PIT join and why do you need it?" — most-recent-feature-value at label time; prevents future-data leak.&lt;/li&gt;
&lt;li&gt;"How do you backfill a new feature?" — rerun the canonical spec over historic partitions and materialize into the online store (see H2 5).&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — feature table partitioned by event_ts
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The canonical offline feature table design. Partitioned by &lt;code&gt;event_ts&lt;/code&gt; (day), clustered by &lt;code&gt;entity_id&lt;/code&gt;, ordered so that PIT joins can prune to just a few days of data. Every batch materialize and every training-data-generation job runs against this table.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Partition column.&lt;/strong&gt; &lt;code&gt;dt = DATE(event_ts)&lt;/code&gt; — daily partitions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clustering / sorting.&lt;/strong&gt; By &lt;code&gt;entity_id&lt;/code&gt; for locality on PIT joins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Row grain.&lt;/strong&gt; One row per &lt;code&gt;(entity_id, event_ts)&lt;/code&gt; pair.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Design the schema and partitioning for a &lt;code&gt;rider_features_offline&lt;/code&gt; table in Snowflake with 500 M rows across 90 days. Show the DDL, the PIT join, and the storage-vs-scan cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Table&lt;/th&gt;
&lt;th&gt;Rows&lt;/th&gt;
&lt;th&gt;Partitions&lt;/th&gt;
&lt;th&gt;Row size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;rider_features_offline&lt;/td&gt;
&lt;td&gt;500 M&lt;/td&gt;
&lt;td&gt;90 daily&lt;/td&gt;
&lt;td&gt;~200 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Snowflake DDL — micro-partition friendly&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;feature_store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rider_features_offline&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;entity_id&lt;/span&gt;      &lt;span class="n"&gt;NUMBER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;event_ts&lt;/span&gt;       &lt;span class="n"&gt;TIMESTAMP_NTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;session_len_sec&lt;/span&gt; &lt;span class="n"&gt;NUMBER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;rides_7d&lt;/span&gt;       &lt;span class="n"&gt;NUMBER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;cancel_rate_7d&lt;/span&gt; &lt;span class="nb"&gt;FLOAT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ltv_score&lt;/span&gt;      &lt;span class="nb"&gt;FLOAT&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;CLUSTER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_ts&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Load partitioned data&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;feature_store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rider_features_offline&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;stg_features&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;07&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;06&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
&lt;span class="n"&gt;FILE_FORMAT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PARQUET&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- PIT join against labels — ASOF join&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;L&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;L&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;L&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_len_sec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rides_7d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cancel_rate_7d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ltv_score&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;labels&lt;/span&gt; &lt;span class="n"&gt;L&lt;/span&gt;
&lt;span class="n"&gt;ASOF&lt;/span&gt;   &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;feature_store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rider_features_offline&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;
       &lt;span class="n"&gt;MATCH_CONDITION&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;L&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_ts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;L&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;L&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_ts&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="s1"&gt;'2026-04-08'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="s1"&gt;'2026-07-06'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- ↑ scans ~90 partitions; per-day pruning&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Cost calculation — Snowflake credit consumption for a 90-day training scan&lt;/span&gt;
&lt;span class="c1"&gt;-- Assume 500 M rows × 200 bytes = 100 GB&lt;/span&gt;
&lt;span class="c1"&gt;-- Snowflake M cluster scans ~1 GB/s per credit&lt;/span&gt;
&lt;span class="c1"&gt;-- 100 GB / 1 GB/s = 100 s ≈ 0.03 credits per scan&lt;/span&gt;
&lt;span class="c1"&gt;-- At $3/credit → $0.09 per training scan&lt;/span&gt;
&lt;span class="c1"&gt;-- Nightly training = $2.70/month&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;CLUSTER BY (DATE(event_ts), entity_id)&lt;/code&gt; tells Snowflake to lay out micro-partitions ordered by day first, then by entity. Any query that filters on &lt;code&gt;event_ts&lt;/code&gt; prunes to just the matching days; any query that also filters on &lt;code&gt;entity_id&lt;/code&gt; prunes within the day. This is the foundation of PIT-join performance.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;COPY INTO&lt;/code&gt; loads a single day's parquet files. In production this is scheduled by Airflow or dbt every day at midnight; the target &lt;code&gt;dt=YYYY-MM-DD&lt;/code&gt; folder in the S3 stage matches the partition semantics.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;ASOF JOIN&lt;/code&gt; is the PIT primitive. For each label row, Snowflake finds the single most recent feature row per entity satisfying &lt;code&gt;L.event_ts &amp;gt;= F.event_ts&lt;/code&gt;. This is &lt;em&gt;exactly&lt;/em&gt; the most recent feature value known at the label's time — no future data leaks in.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;WHERE L.event_ts BETWEEN ...&lt;/code&gt; on the labels side prunes both the labels &lt;em&gt;and&lt;/em&gt; the feature scan (Snowflake's optimiser pushes the range down to the feature table's &lt;code&gt;event_ts&lt;/code&gt; clustering). A 90-day training scan touches ~90 partitions rather than the full 365-day history.&lt;/li&gt;
&lt;li&gt;Cost: 100 GB scanned per full training run at ~1 GB/s per credit → $0.09 per scan. Even nightly training for a month is under $3. The offline store cost is dwarfed by the online store cost (~$220 in the earlier example), which is dwarfed in turn by GPU training cost. Get the partitioning right and the offline store cost is a rounding error.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Table size (500 M rows)&lt;/td&gt;
&lt;td&gt;100 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full-history scan cost&lt;/td&gt;
&lt;td&gt;0.36 credits ≈ $1.08&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;90-day PIT scan cost&lt;/td&gt;
&lt;td&gt;0.09 credits ≈ $0.27&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Partitions touched (90-day)&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Row count returned by PIT join&lt;/td&gt;
&lt;td&gt;1 per (label, entity_id)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Partition by &lt;code&gt;event_ts&lt;/code&gt; day, cluster by &lt;code&gt;entity_id&lt;/code&gt; (or an entity bucket for very high cardinality). The PIT join then scans O(days_in_window) partitions, not O(full history). This is the single largest offline-store cost lever.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — Iceberg feature table on S3 with time travel
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The lake-flavoured equivalent. An Iceberg table on S3 with the same partitioning, queried by Spark or Trino. The killer Iceberg feature is time travel — you can rerun any past training query against the exact snapshot of features that existed on any historic day.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Storage.&lt;/strong&gt; Parquet files under S3, catalogued by Iceberg metadata.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partition spec.&lt;/strong&gt; Hidden partition on &lt;code&gt;days(event_ts)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time travel.&lt;/strong&gt; &lt;code&gt;VERSION AS OF&lt;/code&gt; or &lt;code&gt;TIMESTAMP AS OF&lt;/code&gt; reproduces a past snapshot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Design the Iceberg schema for the same feature table, run a PIT join in Spark, and show a time-travel query that reproduces a past training run for auditability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Table&lt;/th&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;Partition spec&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;feature_store.rider_features_offline&lt;/td&gt;
&lt;td&gt;Iceberg on S3&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;days(event_ts)&lt;/code&gt;, bucket(entity_id, 16)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Iceberg DDL (Spark SQL)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;feature_store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rider_features_offline&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;entity_id&lt;/span&gt;      &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;event_ts&lt;/span&gt;       &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;session_len_sec&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;rides_7d&lt;/span&gt;       &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;cancel_rate_7d&lt;/span&gt; &lt;span class="nb"&gt;FLOAT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ltv_score&lt;/span&gt;      &lt;span class="nb"&gt;FLOAT&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;iceberg&lt;/span&gt;
&lt;span class="n"&gt;PARTITIONED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_ts&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;TBLPROPERTIES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'format-version'&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'write.parquet.compression-codec'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'zstd'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# PIT join in Spark
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pyspark.sql&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SparkSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;functions&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pyspark.sql.window&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Window&lt;/span&gt;

&lt;span class="n"&gt;spark&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SparkSession&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOrCreate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;labels&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;labels&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_ts BETWEEN &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2026-04-08&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; AND &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2026-07-06&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;features&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;feature_store.rider_features_offline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# ASOF via window — find the most recent feature row per (entity, label.event_ts)
&lt;/span&gt;&lt;span class="n"&gt;joined&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;L&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;features&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;F&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;L.entity_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;F.entity_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;
          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;F.event_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;L.event_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;row_number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;over&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;Window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;partitionBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;L.entity_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;L.event_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;F.event_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;desc&lt;/span&gt;&lt;span class="p"&gt;())))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rn = 1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;L.event_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;L.entity_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;L.label&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;F.session_len_sec&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;F.rides_7d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;F.cancel_rate_7d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;F.ltv_score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Time-travel query — reproduce the exact snapshot as of 2026-05-01&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;feature_store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rider_features_offline&lt;/span&gt;
       &lt;span class="k"&gt;VERSION&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;OF&lt;/span&gt; &lt;span class="mi"&gt;12345678&lt;/span&gt;
       &lt;span class="c1"&gt;-- or: TIMESTAMP AS OF '2026-05-01 00:00:00'&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;event_ts&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="s1"&gt;'2026-02-01'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="s1"&gt;'2026-05-01'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Iceberg DDL uses hidden partition transforms — &lt;code&gt;days(event_ts)&lt;/code&gt; and &lt;code&gt;bucket(16, entity_id)&lt;/code&gt;. The user does not write &lt;code&gt;WHERE dt = '2026-07-06'&lt;/code&gt;; the Iceberg planner sees a filter on &lt;code&gt;event_ts&lt;/code&gt; and automatically prunes to the matching day partitions. Hidden partitions eliminate the "forgot to filter on the partition column" bug.&lt;/li&gt;
&lt;li&gt;The Spark PIT join uses a window-function &lt;code&gt;row_number()&lt;/code&gt; per &lt;code&gt;(entity_id, label.event_ts)&lt;/code&gt; ordered by feature &lt;code&gt;event_ts&lt;/code&gt; descending. Row 1 is the most-recent feature value at label time. This is the portable equivalent of Snowflake's &lt;code&gt;ASOF JOIN&lt;/code&gt; — slightly more verbose but produces identical results.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;bucket(16, entity_id)&lt;/code&gt; clause hashes entities into 16 buckets per partition. This provides parallelism &lt;em&gt;within&lt;/em&gt; a partition — Spark can distribute the join across 16 tasks per day. Without bucketing, wide entities create data-skew hotspots.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TIMESTAMP AS OF '2026-05-01 00:00:00'&lt;/code&gt; is Iceberg time travel — the query executes against the exact snapshot that existed on May 1st. This is invaluable for auditability ("reproduce the training data used for model v3.2.1") and for debugging drift ("what did the feature table look like when the model started drifting?").&lt;/li&gt;
&lt;li&gt;The trade-off vs Snowflake: more code (Spark instead of a one-line ASOF), more ops (compaction, snapshot expiry, catalog management), lower per-GB storage, and full portability. If the team already runs Databricks or a Trino cluster, Iceberg wins; if the team runs only Snowflake, the warehouse choice is simpler.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Warehouse (Snowflake)&lt;/th&gt;
&lt;th&gt;Lake (Iceberg on S3)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Storage cost per GB-month&lt;/td&gt;
&lt;td&gt;$0.023 (mgd)&lt;/td&gt;
&lt;td&gt;$0.023 (raw) + ops&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compute cost per training&lt;/td&gt;
&lt;td&gt;$0.27&lt;/td&gt;
&lt;td&gt;$0.20-$1.00 (engine dep.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PIT join syntax&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ASOF JOIN&lt;/code&gt; (1 line)&lt;/td&gt;
&lt;td&gt;window row_number (5 lines)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time travel&lt;/td&gt;
&lt;td&gt;90 days (default)&lt;/td&gt;
&lt;td&gt;unlimited (until expiry)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Portability&lt;/td&gt;
&lt;td&gt;Snowflake only&lt;/td&gt;
&lt;td&gt;Spark, Trino, DuckDB, Athena&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Warehouse if you want the lowest ops overhead and are already paying for the cluster. Lake if you value portability, multi-engine access, and tight cost control. Both are correct; neither is better in the abstract.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — training-data generation from the offline store
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The end-to-end training-data pipeline. Labels come from a &lt;code&gt;labels&lt;/code&gt; table; features come from N feature-view tables; the PIT join stitches them into one wide table that is what the trainer actually consumes. This is the pipeline every ML team runs before every training run.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Input.&lt;/strong&gt; A labels table (&lt;code&gt;label_id, entity_id, event_ts, label&lt;/code&gt;) plus N feature-view tables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output.&lt;/strong&gt; A training frame with one row per label and all features PIT-joined.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost.&lt;/strong&gt; O(labels × features) but partitioned, so bounded by O(days_in_window).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Build the training-data generation pipeline that PIT-joins three feature-view tables to a labels table over 90 days and writes the result to a training frame. Handle the cold-entity case (entity has no feature history) with graceful defaults.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Table&lt;/th&gt;
&lt;th&gt;Rows&lt;/th&gt;
&lt;th&gt;Window&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;labels&lt;/td&gt;
&lt;td&gt;5 M&lt;/td&gt;
&lt;td&gt;90 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rider_session&lt;/td&gt;
&lt;td&gt;500 M&lt;/td&gt;
&lt;td&gt;90 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rider_7d_agg&lt;/td&gt;
&lt;td&gt;100 M&lt;/td&gt;
&lt;td&gt;90 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rider_daily&lt;/td&gt;
&lt;td&gt;20 M&lt;/td&gt;
&lt;td&gt;90 days&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Feast get_historical_features — the canonical PIT join API
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;feast&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FeatureStore&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;

&lt;span class="n"&gt;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FeatureStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;labels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3://acme-ml/labels/2026-04-08_2026-07-06.parquet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# columns: label_id, entity_id, event_ts, label
&lt;/span&gt;
&lt;span class="n"&gt;training&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_historical_features&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;entity_df&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;features&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_session:session_len_sec&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_7d_agg:rides_7d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_7d_agg:cancel_rate_7d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_daily:ltv_score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_df&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Cold-entity handling — fill missing feature values with feature-view defaults
&lt;/span&gt;&lt;span class="n"&gt;DEFAULTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session_len_sec&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rides_7d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cancel_rate_7d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ltv_score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;training&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;training&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillna&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DEFAULTS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;training&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3://acme-ml/train_frame/2026-07-06.parquet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Equivalent hand-written PIT join in Snowflake (for reference)&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;pit&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;L&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;label_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;L&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;L&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;L&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;session_len_sec&lt;/span&gt;
          &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;feature_store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rider_session&lt;/span&gt;
          &lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;entity_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;L&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt;
            &lt;span class="k"&gt;AND&lt;/span&gt;  &lt;span class="n"&gt;event_ts&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;L&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_ts&lt;/span&gt;
          &lt;span class="k"&gt;ORDER&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
          &lt;span class="k"&gt;LIMIT&lt;/span&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;session_len_sec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;rides_7d&lt;/span&gt;
          &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;feature_store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rider_7d_agg&lt;/span&gt;
          &lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;entity_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;L&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt;
            &lt;span class="k"&gt;AND&lt;/span&gt;  &lt;span class="n"&gt;event_ts&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;L&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_ts&lt;/span&gt;
          &lt;span class="k"&gt;ORDER&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
          &lt;span class="k"&gt;LIMIT&lt;/span&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rides_7d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;ltv_score&lt;/span&gt;
          &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;feature_store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rider_daily&lt;/span&gt;
          &lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;entity_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;L&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt;
            &lt;span class="k"&gt;AND&lt;/span&gt;  &lt;span class="n"&gt;event_ts&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;L&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_ts&lt;/span&gt;
          &lt;span class="k"&gt;ORDER&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
          &lt;span class="k"&gt;LIMIT&lt;/span&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;ltv_score&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;labels&lt;/span&gt; &lt;span class="n"&gt;L&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pit&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;event_ts&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="s1"&gt;'2026-04-08'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="s1"&gt;'2026-07-06'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;FeatureStore.get_historical_features(entity_df, features)&lt;/code&gt; is the canonical PIT-join API. Behind the scenes Feast issues an ASOF join per feature-view against the offline store, aligned to the &lt;code&gt;event_ts&lt;/code&gt; in the &lt;code&gt;entity_df&lt;/code&gt;. The output is a wide DataFrame with one row per label and one column per requested feature.&lt;/li&gt;
&lt;li&gt;The API is engine-agnostic — Feast picks up the offline-store type from &lt;code&gt;feature_store.yaml&lt;/code&gt; and dispatches to the right SQL dialect (Snowflake ASOF, BigQuery correlated subquery, Spark window, etc.). The same call runs against every offline store.&lt;/li&gt;
&lt;li&gt;Cold-entity handling: if an entity has no rows in a feature-view before the label's &lt;code&gt;event_ts&lt;/code&gt;, the PIT join returns NULL for that feature. The &lt;code&gt;fillna(DEFAULTS)&lt;/code&gt; step maps NULLs to feature-view-declared defaults so the trainer sees a complete tensor. This is the mechanical equivalent of the "cold-lookup fallback" in the serving path.&lt;/li&gt;
&lt;li&gt;The hand-written SQL variant (shown for reference) uses correlated subqueries per feature. This works but is slow for many features — every feature is a separate subquery. Feast's implementation batches feature-views by grain (one join per grain, not one per feature), which is significantly faster on large label sets.&lt;/li&gt;
&lt;li&gt;The output parquet lives in the training-frame S3 prefix and is versioned by date. The trainer consumes exactly this frame; every training run is reproducible from the (labels partition + feature-store snapshot) pair.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Row count&lt;/th&gt;
&lt;th&gt;Features&lt;/th&gt;
&lt;th&gt;Runtime (Snowflake M cluster)&lt;/th&gt;
&lt;th&gt;Output size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5 M labels&lt;/td&gt;
&lt;td&gt;4 features&lt;/td&gt;
&lt;td&gt;~2 min&lt;/td&gt;
&lt;td&gt;1.5 GB parquet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50 M labels&lt;/td&gt;
&lt;td&gt;4 features&lt;/td&gt;
&lt;td&gt;~15 min&lt;/td&gt;
&lt;td&gt;15 GB parquet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5 M labels&lt;/td&gt;
&lt;td&gt;40 features&lt;/td&gt;
&lt;td&gt;~5 min&lt;/td&gt;
&lt;td&gt;12 GB parquet&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Use the feature-store SDK's &lt;code&gt;get_historical_features&lt;/code&gt; (or the equivalent Tecton / Databricks call) rather than hand-written PIT SQL. The SDK batches joins by grain, dispatches to the right offline store, and handles cold entities uniformly. Hand-written SQL is fine for debugging but not for production training pipelines.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on offline-store choice
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "You're choosing the offline store for a new ML platform. The team already runs Snowflake for analytics, has a small Spark cluster for ad-hoc, and expects to grow to 20 feature views and 5 B rows in the offline store within a year. Walk me through Snowflake vs Iceberg-on-S3 and defend the choice, including the PIT join and backfill implications."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using Iceberg-on-S3 with Snowflake as the primary query engine
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# feature_store.yaml — Iceberg offline, Snowflake as read engine&lt;/span&gt;
&lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mlplatform&lt;/span&gt;
&lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws&lt;/span&gt;
&lt;span class="na"&gt;offline_store&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;iceberg.offline&lt;/span&gt;
  &lt;span class="na"&gt;catalog&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;glue&lt;/span&gt;
  &lt;span class="na"&gt;warehouse&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;s3://acme-fs/warehouse/&lt;/span&gt;
  &lt;span class="na"&gt;s3_endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://s3.us-east-1.amazonaws.com&lt;/span&gt;
&lt;span class="na"&gt;online_store&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dynamodb&lt;/span&gt;
  &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-east-1&lt;/span&gt;
  &lt;span class="na"&gt;table_name_template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;acme_{project}_{name}"&lt;/span&gt;
&lt;span class="na"&gt;registry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;s3://acme-fs/registry.pb&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Feature-view backed by Iceberg with Snowflake read via external table
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;feast&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FeatureView&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ValueType&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;feast.types&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Int64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Float32&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;feast.infra.offline_stores.contrib.iceberg&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;IcebergSource&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;

&lt;span class="n"&gt;rider_7d_agg_source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;IcebergSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;feature_store.rider_7d_agg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;catalog&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;glue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;timestamp_field&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;rider_7d_agg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FeatureView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_7d_agg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;entities&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;rider_entity&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rides_7d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Int64&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cancel_rate_7d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Float32&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
    &lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;rider_7d_agg_source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;online&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Snowflake external Iceberg table — read the same data with Snowflake compute&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="n"&gt;ICEBERG&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;feature_store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rider_7d_agg&lt;/span&gt;
  &lt;span class="n"&gt;EXTERNAL_VOLUME&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'acme_fs_ext_vol'&lt;/span&gt;
  &lt;span class="k"&gt;CATALOG&lt;/span&gt;          &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ICEBERG_CATALOG_GLUE'&lt;/span&gt;
  &lt;span class="n"&gt;CATALOG_TABLE_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'feature_store.rider_7d_agg'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Query with Snowflake's ASOF JOIN — same syntax as native tables&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;L&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;L&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;L&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rides_7d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cancel_rate_7d&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;labels&lt;/span&gt; &lt;span class="n"&gt;L&lt;/span&gt;
&lt;span class="n"&gt;ASOF&lt;/span&gt;   &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;feature_store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rider_7d_agg&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;
       &lt;span class="n"&gt;MATCH_CONDITION&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;L&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_ts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;L&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;L&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_ts&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="s1"&gt;'2026-04-08'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="s1"&gt;'2026-07-06'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;th&gt;Reasoning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Storage format?&lt;/td&gt;
&lt;td&gt;Iceberg on S3&lt;/td&gt;
&lt;td&gt;portability, engine choice, cheap storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Query engine (SQL analysts)?&lt;/td&gt;
&lt;td&gt;Snowflake via external tables&lt;/td&gt;
&lt;td&gt;reuse existing SQL skills&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Query engine (ML training)?&lt;/td&gt;
&lt;td&gt;Spark / Feast on Iceberg direct&lt;/td&gt;
&lt;td&gt;avoid Snowflake credit cost for large scans&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backfill engine?&lt;/td&gt;
&lt;td&gt;Spark (bulk writes to Iceberg)&lt;/td&gt;
&lt;td&gt;best throughput for TB-scale writes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PIT join in Snowflake?&lt;/td&gt;
&lt;td&gt;Yes — &lt;code&gt;ASOF JOIN&lt;/code&gt; on external table&lt;/td&gt;
&lt;td&gt;native syntax; no Spark dependency for analysts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time travel?&lt;/td&gt;
&lt;td&gt;Iceberg snapshot expiry 90 days&lt;/td&gt;
&lt;td&gt;audit + drift debugging&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Iceberg on S3 backed by the Glue catalogue gives the team a single physical store readable by every engine — Snowflake for analysts (external tables), Spark for training (native Iceberg reader), Feast SDK for training-data generation, Trino for ad-hoc. The training pipeline avoids Snowflake credit cost for TB-scale scans; the SQL analysts keep their Snowflake experience. Backfill runs on Spark for throughput.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Surface&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Offline store&lt;/td&gt;
&lt;td&gt;Iceberg on S3 (Glue catalogue)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analytics engine&lt;/td&gt;
&lt;td&gt;Snowflake external Iceberg tables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Training engine&lt;/td&gt;
&lt;td&gt;Spark / Feast direct reader&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage cost&lt;/td&gt;
&lt;td&gt;$0.023/GB-month raw + $0.001/GB-month metadata&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PIT join syntax&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ASOF JOIN&lt;/code&gt; (Snowflake) or window (Spark)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time travel&lt;/td&gt;
&lt;td&gt;90-day snapshot retention&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Format over engine&lt;/strong&gt;&lt;/strong&gt; — Iceberg is the format; Snowflake, Spark, Trino are engines that read the format. Choosing an open format decouples the storage lifetime from the compute vendor. The team can swap engines without moving data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;ASOF JOIN in both engines&lt;/strong&gt;&lt;/strong&gt; — Snowflake's ASOF works against external Iceberg tables; Spark's window-function equivalent produces identical results. The PIT semantics travel with the data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Backfill on Spark, query on Snowflake&lt;/strong&gt;&lt;/strong&gt; — separate compute engines for the write-heavy backfill (Spark) and the read-heavy analytics (Snowflake). Neither cross-contaminates the other's cost model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Time travel for audit&lt;/strong&gt;&lt;/strong&gt; — 90-day snapshot retention means any training run from the last 90 days can be reproduced against the exact feature-table snapshot. This is the auditability requirement for regulated ML (fraud, credit).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — S3 storage ~$2/TB-month; Snowflake external-table scans billed as normal Snowflake credits; Spark cluster billed by hour. The composed cost is typically 30-50% below an all-Snowflake stack for the same workload. O(data_size) storage; O(scans) compute; O(1) format lock-in.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — sql&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL PIT-join and offline-store problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;ETL&lt;/span&gt;
&lt;span&gt;Topic — etl&lt;/span&gt;
&lt;strong&gt;ETL problems on partitioned feature-table design&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  4. Sync — materialization + streaming push
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Materialize for cost, stream for freshness — every sync design is a knob on that axis
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;batch materialization pulls new offline rows into the online store on a schedule (nightly, hourly, every 5 minutes), streaming push writes directly from a source event stream into the online store as events arrive, and the sync design is a knob that trades compute cost against feature freshness, mediated by idempotency to guarantee correctness&lt;/strong&gt;. Every feature-view chooses a lane on this axis.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz716noryrtv2zlajil44.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz716noryrtv2zlajil44.jpeg" alt="Iconographic sync diagram — offline cylinder on the left, two sync paths (batch materialize + streaming push) in the middle, and online cylinder on the right, with an idempotency chip, on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Materialization — batch offline → online.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cadence.&lt;/strong&gt; Nightly (24 h SLA features), hourly (1 h), every 5 minutes (5 min), on-demand (backfill).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mechanics.&lt;/strong&gt; A batch job reads offline rows where &lt;code&gt;event_ts &amp;gt; last_watermark&lt;/code&gt;, transforms if needed, and upserts into the online store. Feast's &lt;code&gt;materialize&lt;/code&gt; command is the canonical implementation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost model.&lt;/strong&gt; O(new_rows) compute per tick; billed by the batch-engine cluster time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When to use.&lt;/strong&gt; Features whose freshness SLA is ≥ 5 minutes; features derived from batch aggregates that only recompute nightly anyway.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Streaming push — event stream → online.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cadence.&lt;/strong&gt; Every event, in near-real-time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mechanics.&lt;/strong&gt; A stream consumer (Kafka, Kinesis, Pubsub) reads events, computes the feature transformation, and writes to the online store. In Feast, this is a &lt;em&gt;streaming FeatureView&lt;/em&gt; backed by a &lt;code&gt;StreamSource&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost model.&lt;/strong&gt; O(events) compute; billed by the streaming cluster's uptime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When to use.&lt;/strong&gt; Features whose freshness SLA is &amp;lt; 1 minute; features that must reflect the very latest event (session length, live session state, sub-minute fraud signals).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Idempotency — the correctness invariant.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The problem.&lt;/strong&gt; Both batch and streaming paths can retry — a materialize job crashes midway and reruns; a streaming consumer restarts and re-reads offsets. Duplicate writes must not corrupt the online-store state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The primitive.&lt;/strong&gt; Upsert with a monotonic timestamp key. &lt;code&gt;PutItem&lt;/code&gt; where the write is a no-op if the current row's &lt;code&gt;event_ts &amp;gt;= incoming.event_ts&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conditional writes.&lt;/strong&gt; DynamoDB conditional expressions (&lt;code&gt;if_not_exists(event_ts)&lt;/code&gt; or &lt;code&gt;attribute_not_exists&lt;/code&gt;) or Redis Lua scripts guard the invariant.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cost trade-off — batch vs streaming.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Batch every 5 min.&lt;/strong&gt; ~100 batch jobs per day × ~$0.05 compute = ~$150/month for a 500 M-row source.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streaming.&lt;/strong&gt; ~$500-2000/month for a Flink/Kafka Streams job running 24/7 that keeps up with 10k events/s.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The knob.&lt;/strong&gt; For SLAs of 5+ minutes, batch is 3-10x cheaper. For sub-minute SLAs, streaming is the only option.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on sync.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Batch vs streaming push — walk me through the trade-off." — cost per row vs freshness.&lt;/li&gt;
&lt;li&gt;"How do you guarantee idempotency?" — monotonic &lt;code&gt;event_ts&lt;/code&gt; on the online row; conditional writes.&lt;/li&gt;
&lt;li&gt;"What's the Feast materialize command?" — &lt;code&gt;feast materialize-incremental $(date -u +%FT%TZ)&lt;/code&gt; — pulls new rows since the last run.&lt;/li&gt;
&lt;li&gt;"How do you handle backfill in the same pipeline?" — same materialize command with &lt;code&gt;--start-date&lt;/code&gt; and &lt;code&gt;--end-date&lt;/code&gt; overrides.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — Feast materialize-incremental command
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The canonical batch sync tool. &lt;code&gt;feast materialize-incremental&lt;/code&gt; reads the feature registry to find all online-enabled FeatureViews, pulls new rows from each's offline source, and upserts them into the online store. The command is idempotent — a rerun with the same end date is a no-op.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Schedule.&lt;/strong&gt; Cron every 5 minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watermark.&lt;/strong&gt; Feast maintains a per-view &lt;code&gt;materialization_intervals&lt;/code&gt; list; the next run reads from the last interval's end.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotency.&lt;/strong&gt; Upserts with &lt;code&gt;event_ts&lt;/code&gt; as the ordering key.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Set up a &lt;code&gt;feast materialize-incremental&lt;/code&gt; cron on a 5-minute cadence for three FeatureViews with different TTLs. Show the cron config, the command, and the watermark bookkeeping.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;FeatureView&lt;/th&gt;
&lt;th&gt;Offline source&lt;/th&gt;
&lt;th&gt;TTL&lt;/th&gt;
&lt;th&gt;Cadence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;rider_session&lt;/td&gt;
&lt;td&gt;Kafka stream&lt;/td&gt;
&lt;td&gt;5 min&lt;/td&gt;
&lt;td&gt;every 1 min (streaming)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rider_7d_agg&lt;/td&gt;
&lt;td&gt;Snowflake table&lt;/td&gt;
&lt;td&gt;1 h&lt;/td&gt;
&lt;td&gt;every 5 min (materialize)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rider_daily&lt;/td&gt;
&lt;td&gt;Snowflake table&lt;/td&gt;
&lt;td&gt;24 h&lt;/td&gt;
&lt;td&gt;nightly (materialize)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# crontab -e&lt;/span&gt;
&lt;span class="c"&gt;# 5-minute cadence for hourly-SLA features&lt;/span&gt;
&lt;span class="k"&gt;*&lt;/span&gt;/5 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; /opt/feast &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  feast materialize-incremental &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; +%FT%TZ&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--views&lt;/span&gt; rider_7d_agg &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /var/log/feast/materialize.log 2&amp;gt;&amp;amp;1

&lt;span class="c"&gt;# nightly for daily-SLA features&lt;/span&gt;
0 3 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; /opt/feast &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  feast materialize-incremental &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; +%FT%TZ&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--views&lt;/span&gt; rider_daily &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /var/log/feast/materialize.log 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# What happens under the hood
# feast/infra/materialization/local_engine.py (simplified)
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;materialize_incremental&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;end_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;views&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FeatureView&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;views&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Get the last successful materialization end
&lt;/span&gt;        &lt;span class="n"&gt;last_end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_last_materialization_end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;start&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;last_end&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ttl_start_date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;] materialize &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; → &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;end_date&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Pull rows from the offline source in the (start, end_date] window
&lt;/span&gt;        &lt;span class="n"&gt;offline_rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;start_ts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;end_ts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;end_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Upsert into the online store
&lt;/span&gt;        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;offline_rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;online_store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;online_write_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Advance watermark
&lt;/span&gt;        &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record_materialization&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Idempotency guard — conditional upsert on DynamoDB
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;upsert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;view_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;features&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ttl_s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;entity_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;UpdateExpression&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SET #ts = :ts, #ttl = :ttl, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; = :&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;features&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;ExpressionAttributeNames&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#ttl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ttl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;features&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;ExpressionAttributeValues&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="n"&gt;event_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:ttl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;event_ts&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ttl_s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;features&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()},&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;ConditionExpression&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attribute_not_exists(event_ts) OR event_ts &amp;lt; :ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;dynamo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exceptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConditionalCheckFailedException&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Newer row already present — safe skip
&lt;/span&gt;        &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The cron fires every 5 minutes with &lt;code&gt;end_date = now&lt;/code&gt;. Feast reads the registry, finds the last successful materialization end for each view, and pulls rows from the offline source in the &lt;code&gt;(last_end, now]&lt;/code&gt; window. The watermark bookkeeping is Feast's responsibility; the operator only picks the cadence.&lt;/li&gt;
&lt;li&gt;The offline query is &lt;code&gt;SELECT * FROM source WHERE event_ts &amp;gt; :last_end AND event_ts &amp;lt;= :end_date&lt;/code&gt;. For a 5-minute window on a 500 M-row table partitioned by day, the query touches one partition and returns O(minutes × rps) rows. On Snowflake this typically runs in seconds.&lt;/li&gt;
&lt;li&gt;The write batches into 1000-row chunks and calls &lt;code&gt;online_write_batch&lt;/code&gt;. Behind the scenes this uses &lt;code&gt;BatchWriteItem&lt;/code&gt; for DynamoDB or a pipelined &lt;code&gt;HSET&lt;/code&gt;/&lt;code&gt;EXPIREAT&lt;/code&gt; for Redis. Each row's &lt;code&gt;event_ts&lt;/code&gt; becomes the &lt;code&gt;ttl&lt;/code&gt; anchor (&lt;code&gt;ttl = event_ts + view.ttl.total_seconds()&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;The conditional upsert (&lt;code&gt;ConditionExpression="attribute_not_exists(event_ts) OR event_ts &amp;lt; :ts"&lt;/code&gt;) is the idempotency invariant. If a rerun writes an older row, the condition fails and the write is a no-op. This makes the materialize job safely rerunnable and immune to backfill overlaps.&lt;/li&gt;
&lt;li&gt;If the job crashes midway, the watermark stays at the last successful &lt;code&gt;end_date&lt;/code&gt;. The next run starts from that watermark and covers the gap. No manual recovery is needed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Materialize latency (5-min window)&lt;/td&gt;
&lt;td&gt;~30 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rows per tick (500 M table, 5-min slice)&lt;/td&gt;
&lt;td&gt;~1.7 M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Online-store writes per tick&lt;/td&gt;
&lt;td&gt;~1.7 M (batched)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idempotency guarantee&lt;/td&gt;
&lt;td&gt;conditional upsert on &lt;code&gt;event_ts&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Watermark storage&lt;/td&gt;
&lt;td&gt;Feast registry (S3)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For any SLA ≥ 5 minutes, run &lt;code&gt;feast materialize-incremental&lt;/code&gt; on a cron matching the SLA. Conditional writes make the job safely rerunnable. Watermarks make crash recovery automatic.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — streaming FeatureView with a Kafka source
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; For sub-minute SLAs, the batch materialize path is too slow. A streaming FeatureView reads from Kafka and writes to the online store on every event. Feast's &lt;code&gt;push&lt;/code&gt; API and Tecton's &lt;code&gt;stream_feature_view&lt;/code&gt; are the canonical shapes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Source.&lt;/strong&gt; Kafka topic &lt;code&gt;rider.session&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transformation.&lt;/strong&gt; Compute &lt;code&gt;session_len_sec = last_click_ts - session_start_ts&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sink.&lt;/strong&gt; DynamoDB &lt;code&gt;rider_session&lt;/code&gt; table, upsert per entity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Implement a streaming FeatureView that consumes from Kafka, computes a session-length feature, and writes to the online store with sub-second freshness. Handle idempotency and out-of-order events.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;Rate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Kafka &lt;code&gt;rider.session&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;JSON&lt;/td&gt;
&lt;td&gt;8k events/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Streaming FeatureView with Feast's push API + Kafka consumer
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;feast&lt;/span&gt;          &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FeatureStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PushMode&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;confluent_kafka&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Consumer&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="n"&gt;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FeatureStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;consumer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Consumer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bootstrap.servers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kafka.internal:9092&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;group.id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fs.rider_session.push&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;enable.auto.commit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;auto.offset.reset&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;earliest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;consumer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider.session&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session_len_sec&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;last_click_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session_start_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;last_click_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;consumer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;poll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;      &lt;span class="k"&gt;continue&lt;/span&gt;

    &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;value&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;row&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# push into online + offline store atomically
&lt;/span&gt;    &lt;span class="n"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;push_source_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_session_stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;PushMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ONLINE_AND_OFFLINE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;consumer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# The push implementation with conditional-write idempotency
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;push_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;view_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Online: upsert only if event_ts is newer
&lt;/span&gt;    &lt;span class="n"&gt;dynamo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;entity_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rider_id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;UpdateExpression&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SET session_len_sec = :s, event_ts = :ts, #ttl = :ttl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ExpressionAttributeNames&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#ttl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ttl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;ExpressionAttributeValues&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session_len_sec&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:ttl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;ConditionExpression&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attribute_not_exists(event_ts) OR event_ts &amp;lt; :ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Offline: append to Iceberg for training-data continuity
&lt;/span&gt;    &lt;span class="nf"&gt;iceberg_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_session_offline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The consumer subscribes to the Kafka topic and polls for events. &lt;code&gt;enable.auto.commit = False&lt;/code&gt; gives the pipeline explicit commit control — the offset is only committed after the online + offline write succeeds, giving at-least-once semantics.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;transform&lt;/code&gt; step computes the feature values from the raw event. For simple aggregations (&lt;code&gt;last_click - session_start&lt;/code&gt;) this is inline; for complex windows the consumer would call into a Flink stateful operator or a windowed Kafka Streams job.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fs.push(to=PushMode.ONLINE_AND_OFFLINE)&lt;/code&gt; writes both stores in one API call — the online store (for serving latency) and the offline store (for training continuity). If the offline store is Iceberg, &lt;code&gt;push&lt;/code&gt; appends to the Iceberg table; if Snowflake, it inserts.&lt;/li&gt;
&lt;li&gt;The conditional upsert (&lt;code&gt;ConditionExpression="attribute_not_exists(event_ts) OR event_ts &amp;lt; :ts"&lt;/code&gt;) is critical for out-of-order events. Kafka partitions do not guarantee cross-partition ordering; a delayed event arriving with an older &lt;code&gt;event_ts&lt;/code&gt; must not overwrite a newer row. The condition guarantees monotonic online writes per entity.&lt;/li&gt;
&lt;li&gt;Latency budget: the p99 from event arrival at Kafka to feature-visible-in-DynamoDB is typically ~200 ms — network + consumer poll + transform + conditional write. For a 60-second freshness SLA this is trivially in budget.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Freshness (event → online)&lt;/td&gt;
&lt;td&gt;p50 100 ms, p99 300 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Throughput&lt;/td&gt;
&lt;td&gt;8k events/s (single consumer)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idempotency&lt;/td&gt;
&lt;td&gt;conditional-write per entity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Out-of-order handling&lt;/td&gt;
&lt;td&gt;older events silently discarded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;At-least-once semantics&lt;/td&gt;
&lt;td&gt;offset commit after write success&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Streaming push wins for freshness SLAs &amp;lt; 1 minute. The conditional-write idempotency is non-negotiable for out-of-order events. Commit offsets after the write, not before — the at-least-once + idempotent-write combo gives effectively-once semantics at the online store level.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — mixed batch + streaming for a single feature view
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Some features have both a batch source (historical backfill) and a streaming source (live updates). Feast's &lt;em&gt;push source&lt;/em&gt; lets one FeatureView receive writes from both — the batch source is the origin of truth for training-data generation and backfill; the streaming source keeps the online store fresh in between batch materializes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The pattern.&lt;/strong&gt; One FeatureView, one online-store row per entity, two write paths.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch path.&lt;/strong&gt; Nightly materialize refreshes the online row with the canonical aggregate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streaming path.&lt;/strong&gt; Sub-minute updates keep the online row fresh between nightlies.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Design a mixed batch + streaming FeatureView for a &lt;code&gt;rider_last_7d_score&lt;/code&gt; feature where the nightly batch computes the canonical aggregate and the streaming path applies incremental updates during the day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Cadence&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Snowflake batch&lt;/td&gt;
&lt;td&gt;nightly&lt;/td&gt;
&lt;td&gt;canonical aggregate; training data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kafka stream&lt;/td&gt;
&lt;td&gt;per event&lt;/td&gt;
&lt;td&gt;incremental updates for freshness&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;feast&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FeatureView&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PushSource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RequestSource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FileSource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PushMode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;feast.types&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Float32&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;

&lt;span class="n"&gt;rider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Entity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Batch source — the training-data origin of truth
&lt;/span&gt;&lt;span class="n"&gt;batch_src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SnowflakeSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;feature_store.rider_last_7d_score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;timestamp_field&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Push source layered on top — same FeatureView, incremental updates
&lt;/span&gt;&lt;span class="n"&gt;push_src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PushSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_last_7d_score_push&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;batch_source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;batch_src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;rider_last_7d_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FeatureView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_last_7d_score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;entities&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;rider&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Float32&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
    &lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;push_src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;online&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pattern&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;batch+stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Nightly batch — canonical materialize
# 0 3 * * * feast materialize-incremental $(date -u +%FT%TZ) --views rider_last_7d_score
&lt;/span&gt;
&lt;span class="c1"&gt;# Streaming push — incremental updates during the day
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_ride_completed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ride&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Recompute the score for the affected rider using rolling window state
&lt;/span&gt;    &lt;span class="n"&gt;new_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compute_delta_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ride&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;push_source_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_last_7d_score_push&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ride&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;new_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ride&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completed_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;}],&lt;/span&gt;
        &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;PushMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ONLINE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;# skip offline; nightly batch is the training origin
&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;PushSource&lt;/code&gt; is Feast's construct for "a feature-view fed by both a batch source and live pushes." The &lt;code&gt;batch_source&lt;/code&gt; inside is the origin of truth — its rows drive &lt;code&gt;materialize&lt;/code&gt; and &lt;code&gt;get_historical_features&lt;/code&gt;. Pushed writes update the online store only (by default).&lt;/li&gt;
&lt;li&gt;Nightly at 03:00 UTC the batch materialize runs; it reads the Snowflake source and overwrites the online row with the canonical daily aggregate. This is the &lt;em&gt;reset point&lt;/em&gt; — every rider's online row is guaranteed to match the offline definition at 03:00.&lt;/li&gt;
&lt;li&gt;During the day, whenever a ride completes, the app pushes an incremental update. &lt;code&gt;PushMode.ONLINE&lt;/code&gt; writes to the online store only; the offline (training) path is not affected. The score reflects the latest ride within seconds.&lt;/li&gt;
&lt;li&gt;The next morning, the batch materialize runs again and overwrites the online row with the fresh canonical aggregate. Any drift accumulated by the streaming path is reset to the batch truth.&lt;/li&gt;
&lt;li&gt;This pattern gives both worlds: training data is the batch canonical (deterministic, PIT-safe); serving data is the batch canonical with intraday streaming freshness applied on top. It is the standard pattern in fraud, rideshare, and recsys platforms.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time of day&lt;/th&gt;
&lt;th&gt;Score source&lt;/th&gt;
&lt;th&gt;Freshness&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;03:00&lt;/td&gt;
&lt;td&gt;nightly batch&lt;/td&gt;
&lt;td&gt;0 s (fresh from batch)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;09:00&lt;/td&gt;
&lt;td&gt;nightly + 3 intraday updates&lt;/td&gt;
&lt;td&gt;~sub-minute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;22:00&lt;/td&gt;
&lt;td&gt;nightly + N intraday updates&lt;/td&gt;
&lt;td&gt;~sub-minute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;03:00 next day&lt;/td&gt;
&lt;td&gt;fresh nightly batch&lt;/td&gt;
&lt;td&gt;reset to 0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Use the batch-plus-push pattern when the &lt;em&gt;training data&lt;/em&gt; wants a stable batch definition and the &lt;em&gt;serving data&lt;/em&gt; wants sub-minute freshness. The nightly batch is the reset point; the intraday push is the accelerator. This is the mainstream production shape.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on sync design
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "You have three feature views — session_length (60s SLA), 7d_rolling_count (15m SLA), and daily_ltv_score (24h SLA). Walk me through the sync design for each: batch vs streaming, cadence, idempotency, and the failure handling. What does the total sync cost look like?"&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using tier-per-SLA sync with idempotent upserts
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# feature_store.yaml declares the three views with different sources
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
project: mlplatform
provider: aws
offline_store: {type: iceberg.offline, catalog: glue, warehouse: s3://acme-fs/}
online_store:  {type: dynamodb,       region: us-east-1}
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="c1"&gt;# Tier 1 — streaming push for session_length (60s SLA)
# Consumer: Kafka rider.session → transform → conditional upsert into DDB
# Cadence: sub-second per event
# Cost: ~$500/month for one Flink task
&lt;/span&gt;
&lt;span class="c1"&gt;# Tier 2 — 5-min batch materialize for 7d_rolling_count (15m SLA)
# */5 * * * * feast materialize-incremental $(date -u +%FT%TZ) --views rider_7d_agg
# Cost: 288 runs/day × $0.05 Snowflake credit = ~$14/day = $420/month
&lt;/span&gt;
&lt;span class="c1"&gt;# Tier 3 — nightly batch materialize for daily_ltv_score (24h SLA)
# 0 3 * * * feast materialize-incremental $(date -u +%FT%TZ) --views rider_daily
# Cost: 1 run/day × $0.20 Snowflake credit = ~$6/month
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Idempotency guard applied to every write path
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;upsert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;features&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ttl_seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Conditional upsert with monotonic event_ts ordering.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;entity_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;UpdateExpression&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SET &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; = :&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ttl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;features&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
            &lt;span class="n"&gt;ExpressionAttributeNames&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ttl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;features&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
            &lt;span class="n"&gt;ExpressionAttributeValues&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:event_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;event_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:ttl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;event_ts&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ttl_seconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;features&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()},&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;ConditionExpression&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attribute_not_exists(event_ts) OR event_ts &amp;lt; :event_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;dynamo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exceptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConditionalCheckFailedException&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;  &lt;span class="c1"&gt;# older event; safe skip
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Cadence&lt;/th&gt;
&lt;th&gt;Cost/month&lt;/th&gt;
&lt;th&gt;Freshness achieved&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;session_length (60s SLA)&lt;/td&gt;
&lt;td&gt;streaming&lt;/td&gt;
&lt;td&gt;per event&lt;/td&gt;
&lt;td&gt;$500 (Flink)&lt;/td&gt;
&lt;td&gt;~200 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7d_rolling_count (15m SLA)&lt;/td&gt;
&lt;td&gt;batch materialize&lt;/td&gt;
&lt;td&gt;every 5 min&lt;/td&gt;
&lt;td&gt;$420 (Snowflake)&lt;/td&gt;
&lt;td&gt;~5 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;daily_ltv_score (24h SLA)&lt;/td&gt;
&lt;td&gt;batch materialize&lt;/td&gt;
&lt;td&gt;nightly&lt;/td&gt;
&lt;td&gt;$6 (Snowflake)&lt;/td&gt;
&lt;td&gt;~24 h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$926&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;all SLAs met&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The tiered design matches each feature's SLA to the cheapest sync path that meets it. Streaming for the sub-minute SLA; 5-minute batch for the intra-hour SLA; nightly for the 24-hour SLA. All three share the same idempotency guard so retries and out-of-order events are safe. Total cost is ~$1000/month for a workload that serves 5000 predictions per second — trivial next to inference GPU cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;session_length freshness&lt;/td&gt;
&lt;td&gt;p99 300 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7d_rolling_count freshness&lt;/td&gt;
&lt;td&gt;p99 5 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;daily_ltv_score freshness&lt;/td&gt;
&lt;td&gt;p99 24 h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idempotency violations&lt;/td&gt;
&lt;td&gt;0 (conditional writes)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failure recovery&lt;/td&gt;
&lt;td&gt;automatic (watermarks + at-least-once)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total monthly sync cost&lt;/td&gt;
&lt;td&gt;~$926&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Tier-per-SLA&lt;/strong&gt;&lt;/strong&gt; — the sync design is not one path; it is one path per SLA tier. Fastest SLA gets streaming (expensive); slowest SLA gets nightly batch (cheap). The tiering matches cost to value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Idempotency invariant&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;ConditionExpression="attribute_not_exists OR event_ts &amp;lt; :ts"&lt;/code&gt; guarantees monotonic online writes regardless of source. Retries, out-of-order events, and backfill overlaps are all safe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Watermarks + at-least-once&lt;/strong&gt;&lt;/strong&gt; — Feast maintains per-view watermarks; Kafka commits offsets after write. Together they give effectively-once semantics at the online-store level despite at-least-once source semantics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Offline continuity&lt;/strong&gt;&lt;/strong&gt; — the batch source for each view is the training-data origin of truth. Streaming push writes to the online store only, so the offline continuity is not fragmented across sources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — O(events) for streaming (Flink cluster); O(rows_per_tick × ticks) for batch (compute credits); O(entities × payload) for online storage. The dominant term for most workloads is the streaming cluster; use batch wherever the SLA allows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Streaming&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — streaming&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Streaming problems on materialize + push sync design&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/streaming" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;ETL&lt;/span&gt;
&lt;span&gt;Topic — etl&lt;/span&gt;
&lt;strong&gt;ETL problems on idempotent batch pipelines&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  5. Freshness budgets + backfill
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Per-feature freshness SLAs plus a backfill wave — the two production disciplines interviewers reserve for senior candidates
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;every feature carries a per-feature freshness SLA (60s / 15m / 1h / 24h), the serving path enforces the SLA at read time, and backfill is the disciplined pipeline that hydrates historic online-store rows when a new feature ships or when the online store cold-starts, without saturating the offline store or the online-store write throughput&lt;/strong&gt;. The senior signal is naming the SLA per feature, designing the backfill wave, and wiring drift monitoring so stale features are caught before they corrupt a model.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw0pn9pakzj449kr7qvzf.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw0pn9pakzj449kr7qvzf.jpeg" alt="Iconographic freshness + backfill diagram — three freshness SLA gauges for online/offline/batch tiers, plus a backfill wave sweep across a historic timeline hydrating the online store, on a light PipeCode card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Freshness SLA per feature — not per platform.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The principle.&lt;/strong&gt; Each feature has its own SLA that reflects how quickly stale-ness poisons the model score. A user's session length is stale in seconds; a user's lifetime score is fresh for a day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The three tiers.&lt;/strong&gt; Sub-minute (streaming), sub-hour (frequent batch), daily (nightly batch). Very rare: strong consistency (read-through per request).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enforcement.&lt;/strong&gt; The serving path compares &lt;code&gt;now - feature.event_ts&lt;/code&gt; against the SLA and treats stale features as missing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Backfill — hydrating historic online rows.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;When you need it.&lt;/strong&gt; New feature ships (online store has no rows); online store cold-starts (Redis flush, DDB table restore); entity list expands (new tenant added); freshness SLA tightens (need finer granularity).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The invariant.&lt;/strong&gt; The backfill must produce the value the online path &lt;em&gt;would have produced&lt;/em&gt; at each &lt;code&gt;event_ts&lt;/code&gt;. Same canonical spec, same window semantics, same defaults.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The wave.&lt;/strong&gt; Backfill sweeps from oldest to newest (or newest to oldest) at a bounded rate to avoid saturating write throughput.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Drift monitoring — catching stale features.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Metric.&lt;/strong&gt; &lt;code&gt;feature_age_seconds{feature="rider_session_length"}&lt;/code&gt; gauge exported per feature per prediction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alert.&lt;/strong&gt; Alert on &lt;code&gt;p99(feature_age_seconds) &amp;gt; sla × 1.5&lt;/code&gt; for 5 minutes — sustained staleness that exceeds the SLA by 50%.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Companion metric.&lt;/strong&gt; &lt;code&gt;feature_null_rate&lt;/code&gt; — the fraction of predictions that fell back to defaults. A rising null rate is the leading indicator of a sync-path failure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cost of a wrong SLA — training/serving skew, again.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Too tight.&lt;/strong&gt; Sync cost balloons; the streaming cluster grows to keep up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Too loose.&lt;/strong&gt; Online serves stale values; model scores drift; users see degraded predictions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Right SLA.&lt;/strong&gt; The point at which the marginal cost of one more tightening equals the marginal model-score improvement — a business trade-off, not just an engineering one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on freshness + backfill.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"What is a per-feature freshness SLA?" — the max age before serving fallback.&lt;/li&gt;
&lt;li&gt;"How do you backfill an online store?" — same canonical spec, sweep with bounded rate.&lt;/li&gt;
&lt;li&gt;"How do you detect stale features in production?" — &lt;code&gt;feature_age_seconds&lt;/code&gt; gauge + &lt;code&gt;feature_null_rate&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;"What's the difference between offline and online freshness?" — offline is batch-materialize cadence; online is per-serving-read enforcement.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — the freshness SLA table
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The freshness SLA table is a one-page artefact every ML platform ships. One row per feature; columns for SLA, sync path, cadence, and fallback. The team commits to it; the on-call monitors it; the model consumers rely on it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Where it lives.&lt;/strong&gt; In the feature-store repo, alongside the feature-view spec.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What it commits to.&lt;/strong&gt; For each feature, a numeric SLA and a fallback plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How it drives design.&lt;/strong&gt; The SLA choice determines the sync path (streaming vs batch vs nightly).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Author the freshness SLA table for a ten-feature ML platform. Show the choices, the sync-path mapping, and the fallback plan for each feature.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Semantics&lt;/th&gt;
&lt;th&gt;Freshness intuition&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;rider_session_length_sec&lt;/td&gt;
&lt;td&gt;current session length&lt;/td&gt;
&lt;td&gt;seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rider_last_click_type&lt;/td&gt;
&lt;td&gt;most recent event kind&lt;/td&gt;
&lt;td&gt;seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rider_recent_5m_ride_count&lt;/td&gt;
&lt;td&gt;rolling 5 min&lt;/td&gt;
&lt;td&gt;1 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rider_recent_1h_cancel_rate&lt;/td&gt;
&lt;td&gt;rolling 1 h&lt;/td&gt;
&lt;td&gt;5 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rider_7d_ride_count&lt;/td&gt;
&lt;td&gt;rolling 7 d&lt;/td&gt;
&lt;td&gt;15 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rider_lifetime_ride_count&lt;/td&gt;
&lt;td&gt;monotonic&lt;/td&gt;
&lt;td&gt;1 h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rider_ltv_score&lt;/td&gt;
&lt;td&gt;ML model output&lt;/td&gt;
&lt;td&gt;24 h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rider_kyc_verified&lt;/td&gt;
&lt;td&gt;compliance&lt;/td&gt;
&lt;td&gt;strong (read-through)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rider_current_geo_country&lt;/td&gt;
&lt;td&gt;live location&lt;/td&gt;
&lt;td&gt;30 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rider_device_fingerprint&lt;/td&gt;
&lt;td&gt;per-login&lt;/td&gt;
&lt;td&gt;strong&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# feature_store.freshness_slas.yaml — the SLA table&lt;/span&gt;
&lt;span class="na"&gt;features&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rider_session_length_sec&lt;/span&gt;
    &lt;span class="na"&gt;sla_seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
    &lt;span class="na"&gt;sync_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;streaming_push&lt;/span&gt;
    &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default(0)&lt;/span&gt;
    &lt;span class="na"&gt;alert_threshold_p99_seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rider_last_click_type&lt;/span&gt;
    &lt;span class="na"&gt;sla_seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
    &lt;span class="na"&gt;sync_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;streaming_push&lt;/span&gt;
    &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default("unknown")&lt;/span&gt;
    &lt;span class="na"&gt;alert_threshold_p99_seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rider_recent_5m_ride_count&lt;/span&gt;
    &lt;span class="na"&gt;sla_seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;
    &lt;span class="na"&gt;sync_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;streaming_push&lt;/span&gt;
    &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default(0)&lt;/span&gt;
    &lt;span class="na"&gt;alert_threshold_p99_seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;120&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rider_recent_1h_cancel_rate&lt;/span&gt;
    &lt;span class="na"&gt;sla_seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt;
    &lt;span class="na"&gt;sync_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;batch_materialize_5m&lt;/span&gt;
    &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default(0.0)&lt;/span&gt;
    &lt;span class="na"&gt;alert_threshold_p99_seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;600&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rider_7d_ride_count&lt;/span&gt;
    &lt;span class="na"&gt;sla_seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;900&lt;/span&gt;
    &lt;span class="na"&gt;sync_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;batch_materialize_5m&lt;/span&gt;
    &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;last_known_value&lt;/span&gt;
    &lt;span class="na"&gt;alert_threshold_p99_seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1800&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rider_lifetime_ride_count&lt;/span&gt;
    &lt;span class="na"&gt;sla_seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3600&lt;/span&gt;
    &lt;span class="na"&gt;sync_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;batch_materialize_hourly&lt;/span&gt;
    &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;last_known_value&lt;/span&gt;
    &lt;span class="na"&gt;alert_threshold_p99_seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;7200&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rider_ltv_score&lt;/span&gt;
    &lt;span class="na"&gt;sla_seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;86400&lt;/span&gt;
    &lt;span class="na"&gt;sync_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;batch_materialize_nightly&lt;/span&gt;
    &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default(0.0)&lt;/span&gt;
    &lt;span class="na"&gt;alert_threshold_p99_seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;129600&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rider_kyc_verified&lt;/span&gt;
    &lt;span class="na"&gt;sla_seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;                       &lt;span class="c1"&gt;# strong consistency&lt;/span&gt;
    &lt;span class="na"&gt;sync_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read_through_service&lt;/span&gt;
    &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default(false)&lt;/span&gt;
    &lt;span class="na"&gt;alert_threshold_p99_seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rider_current_geo_country&lt;/span&gt;
    &lt;span class="na"&gt;sla_seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
    &lt;span class="na"&gt;sync_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;streaming_push&lt;/span&gt;
    &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default("XX")&lt;/span&gt;
    &lt;span class="na"&gt;alert_threshold_p99_seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rider_device_fingerprint&lt;/span&gt;
    &lt;span class="na"&gt;sla_seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
    &lt;span class="na"&gt;sync_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read_through_service&lt;/span&gt;
    &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default(null)&lt;/span&gt;
    &lt;span class="na"&gt;alert_threshold_p99_seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Serving-side enforcement based on the SLA table
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="n"&gt;SLA_TABLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safe_load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;feature_store.freshness_slas.yaml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;features&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_feature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return (usable_value, is_stale).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;sla&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SLA_TABLE&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sla&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sla_seconds&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Strong consistency — no age check; assume caller used read-through
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;event_ts&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;sla&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sla_seconds&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="nf"&gt;emit_stale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sla&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sla_seconds&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;fb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sla&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fallback&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fb&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;last_known_value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;                        &lt;span class="c1"&gt;# serve stale but flag it
&lt;/span&gt;        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;parse_default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fb&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The YAML table is authored once, reviewed by the platform + model owners, and checked into the feature-store repo. Any change requires a review — SLA choices affect both cost (tighter SLA = more sync compute) and model quality (looser SLA = more staleness).&lt;/li&gt;
&lt;li&gt;Each row commits to four things: the SLA, the sync path (which determines cost), the fallback behaviour (what the serving path does when the feature is stale), and the alert threshold (typically 1.5-2x the SLA).&lt;/li&gt;
&lt;li&gt;The serving-side function &lt;code&gt;check_feature&lt;/code&gt; compares &lt;code&gt;now - event_ts&lt;/code&gt; against the SLA. If stale, it emits a metric and applies the fallback: &lt;code&gt;last_known_value&lt;/code&gt; (serve the stale value but flag it) or a hard default (0, "unknown", false).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sla_seconds: 0&lt;/code&gt; is the escape hatch for strong-consistency features — the online store is not authoritative; the caller must read through to a live service (KYC, device fingerprint). The freshness check is bypassed because the read is authoritative by construction.&lt;/li&gt;
&lt;li&gt;The alert threshold &lt;code&gt;p99_seconds&lt;/code&gt; is roughly 2x the SLA. A sustained p99 above threshold means the sync path is falling behind; the on-call is paged before the model starts serving on defaults.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SLA tier&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;th&gt;Sync path&lt;/th&gt;
&lt;th&gt;Estimated cost/month&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;30-60 s&lt;/td&gt;
&lt;td&gt;4 features&lt;/td&gt;
&lt;td&gt;streaming push&lt;/td&gt;
&lt;td&gt;$500 (Flink)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5-15 min&lt;/td&gt;
&lt;td&gt;2 features&lt;/td&gt;
&lt;td&gt;batch materialize 5-min&lt;/td&gt;
&lt;td&gt;$420 (Snowflake)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 h&lt;/td&gt;
&lt;td&gt;1 feature&lt;/td&gt;
&lt;td&gt;batch materialize hourly&lt;/td&gt;
&lt;td&gt;$50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;24 h&lt;/td&gt;
&lt;td&gt;1 feature&lt;/td&gt;
&lt;td&gt;nightly&lt;/td&gt;
&lt;td&gt;$10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;strong&lt;/td&gt;
&lt;td&gt;2 features&lt;/td&gt;
&lt;td&gt;read-through service&lt;/td&gt;
&lt;td&gt;$0 (delegated)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Author the SLA table before you write the FeatureView specs. The SLA drives the sync path; the sync path drives the cost. Working the other way (spec first, SLA later) leads to over-engineering the sync for features that could tolerate a looser SLA.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — backfilling a new feature into the online store
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A team ships a new feature &lt;code&gt;rider_7d_ride_count&lt;/code&gt;. The offline table has 90 days of history; the online store is empty. Before the feature is enabled in the serving path, the team must backfill 90 days of history into the online store so that no rider serves NULL on day one. The backfill sweep is the disciplined pipeline that does this without saturating anything.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The scope.&lt;/strong&gt; 90 days × 20 M entities = ~1.8 B write operations to online store.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The rate limit.&lt;/strong&gt; DDB on-demand can absorb ~40k writes/s; a naive backfill saturates the write budget.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The wave.&lt;/strong&gt; Sweep from oldest to newest at a bounded rate; use conditional writes so newer values are never overwritten.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Design the backfill for a new feature that needs 90 days of history hydrated into the online store. Show the sweep loop, the rate limit, and the completion check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Distinct entities&lt;/td&gt;
&lt;td&gt;20 M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;History days&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sample rate (offline rows per entity per day)&lt;/td&gt;
&lt;td&gt;~1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total offline rows&lt;/td&gt;
&lt;td&gt;1.8 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Online write rate budget&lt;/td&gt;
&lt;td&gt;40k writes/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backfill duration budget&lt;/td&gt;
&lt;td&gt;12 h&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Backfill sweep — one day at a time, newest to oldest
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;feast&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FeatureStore&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="n"&gt;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FeatureStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;END&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;utcnow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;START&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;END&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Feast materialize accepts (start, end) — sweep in reverse for freshness-priority
# (newest rows appear in online store first; older rows follow)
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;backfill_range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk_hours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Sweep the range in chunk-hour slices, throttling to stay under the write budget.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;cur_end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;cur_end&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur_start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cur_end&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;chunk_hours&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;materialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;start_date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cur_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;end_date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cur_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;feature_views&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;dur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;] &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cur_start&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; → &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cur_end&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; materialized in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;dur&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;cur_end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cur_start&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Rate-limited writer for direct backfill (bypassing Feast for control)
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;concurrent.futures&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ThreadPoolExecutor&lt;/span&gt;

&lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dynamodb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nc"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;acme_fs_rider_7d_agg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;TARGET_WRITES_PER_SEC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;35000&lt;/span&gt;        &lt;span class="c1"&gt;# under the 40k budget for headroom
&lt;/span&gt;&lt;span class="n"&gt;POOL&lt;/span&gt;                  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_workers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rate_limited_bulk_write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Write rows to DDB at ~TARGET_WRITES_PER_SEC using batched writers.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;written&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;chunk_iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;POOL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_batch_write&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;written&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# simple token-bucket pacing
&lt;/span&gt;        &lt;span class="n"&gt;expected_elapsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;written&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;TARGET_WRITES_PER_SEC&lt;/span&gt;
        &lt;span class="n"&gt;actual_elapsed&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;actual_elapsed&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;expected_elapsed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected_elapsed&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;actual_elapsed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_batch_write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;batch_writer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Item&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Completion check — verify online store coverage before enabling the feature&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;covered_entities&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;feature_store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rider_7d_agg_offline&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;event_ts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'90 days'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- ↑ expected: 20 M&lt;/span&gt;

&lt;span class="c1"&gt;-- Sample the online store — pick 1000 random entities and check they have rows&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;sample&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;entity_id&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;feature_store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rider_7d_agg_offline&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;event_ts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'1 day'&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;RANDOM&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;LIMIT&lt;/span&gt;  &lt;span class="mi"&gt;1000&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;-- (Application code queries DDB for each entity and asserts a row exists)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The sweep splits the 90-day range into 6-hour chunks and materializes each chunk end-to-start (newest first). "Newest first" means the online store has the most recent data available earliest, which is what the serving path needs if the feature is partially enabled during the backfill.&lt;/li&gt;
&lt;li&gt;The rate-limited writer uses a token-bucket to hold write throughput at 35k writes/s — 12.5% below the 40k budget for headroom. A &lt;code&gt;ThreadPoolExecutor&lt;/code&gt; gives parallelism (64 workers) without saturating the local Python event loop.&lt;/li&gt;
&lt;li&gt;The conditional-upsert idempotency guard (from the sync section) applies to the backfill writes too. If a streaming push has already written a newer row for an entity, the backfill's older row is silently skipped. This makes the backfill and the live sync safe to run concurrently.&lt;/li&gt;
&lt;li&gt;Completion is verified in two ways: (a) offline query counts distinct entities in the 90-day window (expected: 20 M); (b) a random sample of 1000 entities is checked in the online store to confirm every one has a row. Only when both checks pass is the feature enabled in the serving path.&lt;/li&gt;
&lt;li&gt;Backfill duration math: 1.8 B rows / 35k rps ≈ 14.3 hours. If the budget is 12 h, raise the write rate to 42k (approaching the DDB budget) or run two backfill workers in parallel. If the budget is 24 h, the current pace is comfortable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rows to backfill&lt;/td&gt;
&lt;td&gt;1.8 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write rate&lt;/td&gt;
&lt;td&gt;35k/s (12.5% under budget)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backfill duration&lt;/td&gt;
&lt;td&gt;~14 h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coverage after backfill&lt;/td&gt;
&lt;td&gt;20 M distinct entities&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concurrent live sync?&lt;/td&gt;
&lt;td&gt;yes (conditional writes protect)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Backfill is a disciplined sweep, not a firehose. Bound the write rate under the online-store budget; run oldest-to-newest or newest-to-first based on business priority; use conditional writes so the backfill is safe to run alongside the live sync. Verify completion with a sample check before enabling the feature.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — drift monitoring for stale features
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The final production discipline. Every prediction emits a &lt;code&gt;feature_age_seconds&lt;/code&gt; gauge per feature; the on-call dashboard shows the p99 age per feature; alerts fire when p99 exceeds the SLA + margin. A rising &lt;code&gt;feature_null_rate&lt;/code&gt; is the leading indicator — features start falling back to defaults before the model score drifts far enough to trigger a business-level alarm.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Metrics.&lt;/strong&gt; &lt;code&gt;feature_age_seconds&lt;/code&gt; (gauge, per feature), &lt;code&gt;feature_null_rate&lt;/code&gt; (counter, per feature).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dashboard.&lt;/strong&gt; One panel per SLA tier; p99 age plotted against SLA.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alerts.&lt;/strong&gt; &lt;code&gt;p99(feature_age_seconds) &amp;gt; sla × 1.5&lt;/code&gt; for 5 min; &lt;code&gt;feature_null_rate &amp;gt; 0.01&lt;/code&gt; for 5 min.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Instrument the serving path to emit per-feature age gauges and null-rate counters. Show the Prometheus wiring and the alert configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Labels&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;feature_age_seconds&lt;/td&gt;
&lt;td&gt;histogram&lt;/td&gt;
&lt;td&gt;feature_name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;feature_null_rate&lt;/td&gt;
&lt;td&gt;counter&lt;/td&gt;
&lt;td&gt;feature_name, cause&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Serving-side instrumentation
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;prometheus_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Histogram&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start_http_server&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;yaml&lt;/span&gt;

&lt;span class="nf"&gt;start_http_server&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;9091&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;FEATURE_AGE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Histogram&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;feature_age_seconds&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;age of feature at read time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;feature_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;buckets&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;900&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;86400&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;FEATURE_NULL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;feature_null_total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;features that fell back to null/default&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;feature_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cause&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;   &lt;span class="c1"&gt;# cause = "stale" | "cold" | "read_error"
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;SLA_TABLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safe_load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;freshness_slas.yaml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;features&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;enforce_freshness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;feature_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;sla&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SLA_TABLE&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;feature_name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;event_ts&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;FEATURE_NULL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;feature_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cold&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;inc&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;parse_default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sla&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fallback&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;event_ts&lt;/span&gt;
    &lt;span class="n"&gt;FEATURE_AGE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;feature_name&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sla&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sla_seconds&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;sla&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sla_seconds&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;FEATURE_NULL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;feature_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stale&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;inc&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sla&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fallback&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;last_known_value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;              &lt;span class="c1"&gt;# serve stale but recorded
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;parse_default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sla&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fallback&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# prometheus alerts&lt;/span&gt;
&lt;span class="na"&gt;groups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;feature_store_freshness&lt;/span&gt;
    &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;alert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;FeatureStale&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;histogram_quantile(0.99,&lt;/span&gt;
            &lt;span class="s"&gt;sum by (le, feature_name) (&lt;/span&gt;
              &lt;span class="s"&gt;rate(feature_age_seconds_bucket[5m])&lt;/span&gt;
            &lt;span class="s"&gt;)&lt;/span&gt;
          &lt;span class="s"&gt;)&lt;/span&gt;
          &lt;span class="s"&gt;&amp;gt; on(feature_name)&lt;/span&gt;
          &lt;span class="s"&gt;(feature_freshness_sla_seconds * 1.5)&lt;/span&gt;
        &lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5m&lt;/span&gt;
        &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;warning&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;feature&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$labels.feature_name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;is&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;stale&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(p99&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;age&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1.5x&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;SLA)"&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;alert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;FeatureNullSpike&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;sum by (feature_name) (rate(feature_null_total[5m]))&lt;/span&gt;
          &lt;span class="s"&gt;/ sum by (feature_name) (rate(feature_reads_total[5m]))&lt;/span&gt;
          &lt;span class="s"&gt;&amp;gt; 0.01&lt;/span&gt;
        &lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5m&lt;/span&gt;
        &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;warning&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;feature&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$labels.feature_name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;null&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;rate&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1%&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(fallback&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;triggering)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;feature_age_seconds&lt;/code&gt; is a Prometheus histogram with per-feature labels and buckets chosen to span the SLA tiers (1 s, 5 s, 15 s, ... 24 h). &lt;code&gt;Histogram.observe(age)&lt;/code&gt; records each read; &lt;code&gt;histogram_quantile(0.99, ...)&lt;/code&gt; computes the p99 in PromQL.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;feature_null_total&lt;/code&gt; is a counter incremented whenever the serving path falls back to a default. The &lt;code&gt;cause&lt;/code&gt; label ("stale", "cold", "read_error") distinguishes the reasons — a rising "stale" count means the sync path is falling behind; a rising "cold" count means new entities are appearing faster than the backfill can hydrate.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;enforce_freshness&lt;/code&gt; is the serving-side wrapper around every online-store read. It observes the age gauge, checks the SLA, applies the fallback, and returns the usable value. Every model input flows through this function.&lt;/li&gt;
&lt;li&gt;The first alert (&lt;code&gt;FeatureStale&lt;/code&gt;) compares the p99 age gauge to &lt;code&gt;sla × 1.5&lt;/code&gt;. A five-minute sustained breach means the sync path is systematically failing to meet the SLA, not just a transient blip. The on-call diagnoses the sync job — Kafka lag, Snowflake query slowdown, or DDB throttling.&lt;/li&gt;
&lt;li&gt;The second alert (&lt;code&gt;FeatureNullSpike&lt;/code&gt;) fires when more than 1% of reads for a feature fall back. This is the leading indicator; the model score has not necessarily drifted yet, but it will if the null rate keeps rising. The alert gives the on-call a head start.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Panel&lt;/th&gt;
&lt;th&gt;Alert&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;feature_age_seconds p99&lt;/td&gt;
&lt;td&gt;one line per feature&lt;/td&gt;
&lt;td&gt;&amp;gt; sla × 1.5 for 5m&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;feature_null_total rate&lt;/td&gt;
&lt;td&gt;per-feature stacked bar&lt;/td&gt;
&lt;td&gt;&amp;gt; 1% for 5m&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;feature_reads_total rate&lt;/td&gt;
&lt;td&gt;total serving traffic&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;feature_cold_total rate&lt;/td&gt;
&lt;td&gt;per-feature&lt;/td&gt;
&lt;td&gt;&amp;gt; 0.001 for 15m (backfill lag)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Every feature emits an age gauge and a null-rate counter. The on-call dashboard shows one panel per SLA tier. Alerts fire on &lt;code&gt;p99_age &amp;gt; sla × 1.5&lt;/code&gt; (sync falling behind) and &lt;code&gt;null_rate &amp;gt; 1%&lt;/code&gt; (fallback triggering). Combined, they catch every staleness bug before it corrupts a model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on freshness + backfill discipline
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "You are shipping a new feature into a production ML platform. Walk me through the freshness SLA choice, the backfill plan for 90 days of history, and the drift-monitoring you'd wire up. What does the runbook look like when the drift alert fires?"&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using SLA-first design, throttled backfill, and drift-alert runbook
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Step 1 — declare the SLA in the freshness table&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rider_recent_10m_score&lt;/span&gt;
  &lt;span class="na"&gt;sla_seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;
  &lt;span class="na"&gt;sync_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;streaming_push&lt;/span&gt;
  &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;last_known_value&lt;/span&gt;
  &lt;span class="na"&gt;alert_threshold_p99_seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;120&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Step 2 — deploy the FeatureView with the streaming source (dark-launched)
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;feast&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FeatureView&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PushSource&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;feast.types&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Float32&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;

&lt;span class="n"&gt;rider_recent_10m_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FeatureView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_recent_10m_score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;entities&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;rider&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Float32&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
    &lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;PushSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_10m_score_push&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch_source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;snowflake_batch_src&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;online&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;launch_state&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shadow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Step 3 — backfill 90 days of history at a bounded rate
# (Runs before the feature is enabled in the serving path)
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;

&lt;span class="n"&gt;END&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;START&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;utcnow&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;utcnow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;CHUNK&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;cur_end&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;END&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;cur_end&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;START&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;cur_start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;START&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cur_end&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;CHUNK&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;materialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cur_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cur_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;feature_views&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rider_recent_10m_score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;cur_end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cur_start&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;# throttle to avoid DDB write saturation
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Step 4 — enable drift monitoring&lt;/span&gt;
&lt;span class="c1"&gt;# (feature_age_seconds histogram + feature_null_total counter, alerts as shown above)&lt;/span&gt;

&lt;span class="c1"&gt;# Step 5 — dark-launch: serve the feature but do not include it in the model output for 1 week&lt;/span&gt;
&lt;span class="c1"&gt;#          verify feature_age_seconds p99 stays under 60s; verify feature_null_rate stays under 0.1%&lt;/span&gt;
&lt;span class="c1"&gt;#          only then flip the model to consume the feature&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Runbook — "FeatureStale rider_recent_10m_score p99 age 180s"
=============================================================
t+0s     Alert fires; PagerDuty pages on-call ML platform
t+30s    Check Prometheus panel — confirm p99 is elevated across all pods (not one bad pod)
t+60s    Check the streaming push job status — Kafka consumer lag &amp;gt; 0?
         - If yes: streaming push is falling behind; check Flink task metrics
         - If no: check DDB throttling metrics
t+120s   Inspect Kafka topic — is upstream producing at expected rate?
         - If producer is dead: page upstream owner
         - If producer OK: check the push transform for a stuck offset
t+180s   Mitigation options:
         a) Restart the Flink task (typical fix)
         b) Scale up parallelism (if lag is growing)
         c) Flip serving to fallback for this feature (if root cause &amp;gt; 30 min)
t+300s   File ticket, capture Kafka lag graph + Flink checkpoint history for RCA
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Activity&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Declare SLA in freshness table&lt;/td&gt;
&lt;td&gt;Contract in repo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Deploy FeatureView (shadow)&lt;/td&gt;
&lt;td&gt;View live, not consumed by model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Backfill 90 days (throttled)&lt;/td&gt;
&lt;td&gt;20 M entities × 90 d hydrated in ~14 h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Wire drift monitoring&lt;/td&gt;
&lt;td&gt;age gauge + null counter + alert&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Dark launch for 1 week&lt;/td&gt;
&lt;td&gt;verify p99 age and null rate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Enable in model&lt;/td&gt;
&lt;td&gt;model consumes the feature&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runbook&lt;/td&gt;
&lt;td&gt;Drift alert fires&lt;/td&gt;
&lt;td&gt;on-call diagnoses sync path&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After launch, the feature has an SLA the platform commits to, a backfill that hydrated the online store without saturating anything, drift monitoring that will catch any staleness before it poisons the model, and a runbook the on-call can execute in five minutes. The senior signal is doing all five steps &lt;em&gt;before&lt;/em&gt; enabling the feature — not doing them after the first incident.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Discipline&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SLA declared&lt;/td&gt;
&lt;td&gt;yes (60 s streaming)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backfill complete&lt;/td&gt;
&lt;td&gt;90 d × 20 M entities&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drift monitoring&lt;/td&gt;
&lt;td&gt;age gauge + null counter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alert wired&lt;/td&gt;
&lt;td&gt;p99 &amp;gt; 90 s for 5m&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runbook&lt;/td&gt;
&lt;td&gt;linked from alert&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dark launch&lt;/td&gt;
&lt;td&gt;1 week, no incidents&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;SLA-first design&lt;/strong&gt;&lt;/strong&gt; — the SLA drives every downstream decision: sync path, cost, alert threshold, fallback. Skipping the SLA declaration is how features ship without an operational contract and how drift bugs go unnoticed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Throttled backfill&lt;/strong&gt;&lt;/strong&gt; — a bounded-rate sweep protects the online-store write budget from the backfill wave. Conditional writes make the sweep safe to run alongside the live sync path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Drift monitoring + null-rate&lt;/strong&gt;&lt;/strong&gt; — the age gauge catches sync-path slowdowns; the null-rate counter catches serving-side fallback storms. Together they cover both directions of the staleness failure mode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Dark launch&lt;/strong&gt;&lt;/strong&gt; — deploying the feature without consuming it in the model gives the platform a full week to catch drift issues before any production score depends on the feature. The cost is one week of engineering time; the avoided cost is one model-quality incident.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — SLA-first design is O(1) up-front cost (one YAML row); throttled backfill is O(rows / write_rate) hours of Snowflake + DDB compute; drift monitoring is O(reads) Prometheus overhead (~0.5% of serving CPU). All of it is dwarfed by the avoided cost of a stale-feature-driven model drift incident.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Streaming&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — streaming&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Streaming problems on freshness SLA and drift&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/streaming" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;


&lt;p&gt;&lt;span&gt;Optimization&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — optimization&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Optimization problems on backfill sweep and throttling&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/optimization" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;





&lt;h2&gt;
  
  
  Cheat sheet — feature store sync recipes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two-store rule.&lt;/strong&gt; Every serious ML platform runs an offline store (warehouse or lake) for training + PIT joins + backfill and an &lt;code&gt;online feature store&lt;/code&gt; (Redis, DynamoDB, or Cassandra) for serving. Never serve from a warehouse; never train from a KV. The materialization job bridges the two.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis online-store payload schema.&lt;/strong&gt; &lt;code&gt;HSET rider_session:42 session_len_sec 320 event_ts 1720000000&lt;/code&gt; + &lt;code&gt;EXPIREAT rider_session:42 1720000300&lt;/code&gt;. Small hashes (&amp;lt; 4 KB) are ziplist-encoded and 3-4x more memory-efficient. Batch writes with a pipeline; &lt;code&gt;transaction=False&lt;/code&gt; gives throughput without cross-entity atomicity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DynamoDB online-store item schema.&lt;/strong&gt; &lt;code&gt;PutItem&lt;/code&gt; with &lt;code&gt;entity_id&lt;/code&gt; as partition key, feature attributes, and a &lt;code&gt;ttl&lt;/code&gt; attribute (&lt;code&gt;event_ts + budget_seconds&lt;/code&gt;). TTL expiry is eventual within 48 h; always verify freshness at read time with &lt;code&gt;now - event_ts&lt;/code&gt;. Payload cap 400 KB; compress or split larger items.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cassandra online-store schema.&lt;/strong&gt; &lt;code&gt;CREATE KEYSPACE fs WITH REPLICATION = {'class': 'NetworkTopologyStrategy', 'us_east': 3, 'us_west': 3}&lt;/code&gt;. &lt;code&gt;INSERT ... USING TTL 300&lt;/code&gt; for per-column TTL. Read with &lt;code&gt;LOCAL_QUORUM&lt;/code&gt; for regional p99 &amp;lt; 15 ms. Use only for active-active multi-region.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offline table partitioning.&lt;/strong&gt; Always partition by &lt;code&gt;event_ts&lt;/code&gt; day; cluster or bucket by &lt;code&gt;entity_id&lt;/code&gt;. Snowflake: &lt;code&gt;CLUSTER BY (DATE(event_ts), entity_id)&lt;/code&gt;. Iceberg: &lt;code&gt;PARTITIONED BY (days(event_ts), bucket(16, entity_id))&lt;/code&gt;. Any PIT scan then touches only the relevant partitions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PIT join primitive.&lt;/strong&gt; Snowflake: &lt;code&gt;ASOF JOIN F MATCH_CONDITION(L.event_ts &amp;gt;= F.event_ts) ON L.entity_id = F.entity_id&lt;/code&gt;. Spark/Iceberg: window-function &lt;code&gt;row_number() OVER (PARTITION BY entity_id, label_ts ORDER BY feature_ts DESC)&lt;/code&gt; with &lt;code&gt;WHERE feature_ts &amp;lt;= label_ts&lt;/code&gt;. Both produce the most-recent feature value known at label time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feast materialize command.&lt;/strong&gt; &lt;code&gt;feast materialize-incremental $(date -u +%FT%TZ) --views rider_7d_agg&lt;/code&gt;. Reads the last watermark from the registry, pulls new offline rows, upserts into the online store, records the new watermark. Idempotent; safe to rerun.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streaming FeatureView spec.&lt;/strong&gt; &lt;code&gt;PushSource(name="rider_stream", batch_source=snowflake_src)&lt;/code&gt; inside a &lt;code&gt;FeatureView(source=push_src, online=True)&lt;/code&gt;. Consumer calls &lt;code&gt;fs.push(push_source_name="rider_stream", df=[row], to=PushMode.ONLINE_AND_OFFLINE)&lt;/code&gt;. Batch source remains the training origin; push updates the online store live.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotent upsert.&lt;/strong&gt; DDB &lt;code&gt;ConditionExpression="attribute_not_exists(event_ts) OR event_ts &amp;lt; :ts"&lt;/code&gt;. Redis Lua script that compares &lt;code&gt;HGET key event_ts&lt;/code&gt; before &lt;code&gt;HSET&lt;/code&gt;. Never overwrite a newer row; never fail on a duplicate write. Works for retries, out-of-order events, and backfill overlaps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Freshness SLA table.&lt;/strong&gt; One YAML row per feature: &lt;code&gt;sla_seconds&lt;/code&gt;, &lt;code&gt;sync_path&lt;/code&gt; (streaming_push / batch_materialize_5m / batch_materialize_hourly / nightly / read_through_service), &lt;code&gt;fallback&lt;/code&gt; (default value / last_known_value), &lt;code&gt;alert_threshold_p99_seconds&lt;/code&gt;. Author before writing the FeatureView spec.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backfill script skeleton.&lt;/strong&gt; Sweep offline range in 6-hour chunks (newest-to-oldest for freshness priority). Rate-limit writes with a token bucket at ~85% of the online-store write budget. Use conditional writes so live sync and backfill can run concurrently. Verify completion with a random-sample check before enabling the feature.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drift monitoring.&lt;/strong&gt; &lt;code&gt;feature_age_seconds&lt;/code&gt; Prometheus histogram per feature; &lt;code&gt;feature_null_total&lt;/code&gt; counter with &lt;code&gt;cause&lt;/code&gt; label (stale / cold / read_error). Alert on &lt;code&gt;p99(feature_age_seconds) &amp;gt; sla × 1.5 for 5m&lt;/code&gt; and &lt;code&gt;null_rate &amp;gt; 1% for 5m&lt;/code&gt;. Dashboard: one panel per SLA tier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dark launch.&lt;/strong&gt; Deploy a new FeatureView + backfill + monitoring, but do not include the feature in the model output for one week. Verify age p99 and null-rate stay in-SLA. Only then flip the model to consume it. Catches sync-path bugs before any prediction depends on the new feature.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feast vs Tecton vs Databricks.&lt;/strong&gt; Feast is open-source, BYO-infra, works with any offline + online store. Tecton is managed, includes streaming compute, batteries-included but vendor-owned. Databricks FS is native to Delta + Unity Catalog, best if you already run Databricks. All three model the offline/online split; the differences are in ops and integration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When each online-store backend wins.&lt;/strong&gt; Redis for sub-ms and hot working set that fits in RAM; DynamoDB for managed cloud and predictable single-digit-ms; Cassandra/Scylla for active-active multi-region high-write; vector store sidecar for embedding features with ANN needs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is an online feature store and when do I need one?
&lt;/h3&gt;

&lt;p&gt;An &lt;strong&gt;&lt;code&gt;online feature store&lt;/code&gt;&lt;/strong&gt; is a bounded key-value tier — Redis, DynamoDB, or Cassandra typically — that answers "give me feature X for entity Y right now" in single-digit milliseconds. You need one whenever the serving path cannot afford the seconds-scale latency of a warehouse query, which is any production ML platform serving more than a handful of predictions per second. The offline store (a warehouse or lake table partitioned by &lt;code&gt;event_ts&lt;/code&gt;) remains the source of truth for training and PIT joins; the online store is the sub-ms serving accelerator that carries only the current feature values per entity. The two are synchronised via batch materialization, streaming push, or a mix — the &lt;code&gt;feature store sync&lt;/code&gt; design is the contract that keeps them coherent. Skipping the online store and serving directly from a warehouse is the #1 architectural mistake teams make when they conflate "we already have a data warehouse" with "we have a feature store" — the workloads have opposite performance profiles.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need Redis or DynamoDB for the online tier?
&lt;/h3&gt;

&lt;p&gt;Pick &lt;strong&gt;Redis&lt;/strong&gt; when the working set fits in RAM (a few hundred GB at most for practical single-cluster deployments), sub-millisecond p99 is a hard requirement, and the team has Redis operational lore. Pick &lt;strong&gt;DynamoDB&lt;/strong&gt; when you want managed cloud operations, single-digit-ms p99 is fast enough, spiky workloads need auto-scale without operator intervention, and multi-region active-passive is a requirement (global tables). Pick &lt;strong&gt;Cassandra/Scylla&lt;/strong&gt; when you need active-active multi-region writes or the write rate exceeds ~50k writes/s. In practice, the majority of new 2026 platforms start on DynamoDB for the operational simplicity, add a Redis sidecar for the hottest 5% of entities when p99 latency starts to matter, and only move to Cassandra when the multi-region-write requirement becomes real. Do not choose Redis because "it is faster" without confirming the working set fits in RAM and the ops discipline is real; do not choose DynamoDB and then complain about the 400 KB payload limit.&lt;/p&gt;

&lt;h3&gt;
  
  
  How often should I sync the online store from the offline store?
&lt;/h3&gt;

&lt;p&gt;The right answer is &lt;strong&gt;per-feature, not per-platform&lt;/strong&gt; — every feature has its own &lt;code&gt;feature freshness&lt;/code&gt; SLA and the sync cadence should match. Sub-minute SLA features (live session, recent click, real-time score) use streaming push from a Kafka source with sub-second freshness; 5-15 minute SLA features (rolling counts, recent aggregates) use &lt;code&gt;feast materialize-incremental&lt;/code&gt; on a 5-minute cron; hourly SLA features use hourly cron; daily SLA features (LTV score, demographic scores) use a nightly materialize. Author the freshness SLA table first — a YAML row per feature declaring &lt;code&gt;sla_seconds&lt;/code&gt;, &lt;code&gt;sync_path&lt;/code&gt;, &lt;code&gt;fallback&lt;/code&gt;, and &lt;code&gt;alert_threshold&lt;/code&gt; — and let it drive the sync-cadence decisions. Skipping the SLA table and syncing everything at "5 minutes because that felt right" is how sync costs balloon; only the sub-minute features need streaming, and only the sub-hour features need the 5-minute batch cadence.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is a freshness budget and how do I enforce it?
&lt;/h3&gt;

&lt;p&gt;A &lt;strong&gt;freshness budget&lt;/strong&gt; (also called freshness SLA) is the maximum age a feature can carry before it is unfit for serving — 60 seconds for a session-length feature, 15 minutes for a 7-day rolling aggregate, 24 hours for a nightly LTV score. Enforcement is per-read: every serving path reads the feature value &lt;em&gt;and&lt;/em&gt; its &lt;code&gt;event_ts&lt;/code&gt;, computes &lt;code&gt;age = now - event_ts&lt;/code&gt;, and compares against the SLA. Stale features either fall back to a default (0, "unknown"), serve the last-known value with a flag, or trigger a synchronous read-through to a live service. The Prometheus &lt;code&gt;feature_age_seconds&lt;/code&gt; histogram exports the age per feature; the &lt;code&gt;feature_null_total&lt;/code&gt; counter tracks fallbacks; alerts fire when &lt;code&gt;p99(feature_age_seconds) &amp;gt; sla × 1.5&lt;/code&gt; for 5 minutes. Never trust the online store's TTL as the freshness guarantee — DynamoDB's TTL is eventual within 48 h, Redis's is exact but only expires on-access. Always verify at read time.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I backfill an online feature store for a new feature?
&lt;/h3&gt;

&lt;p&gt;A &lt;strong&gt;&lt;code&gt;feature backfill&lt;/code&gt;&lt;/strong&gt; hydrates historic online-store rows so a new feature can serve immediately at launch rather than returning NULL for every entity. The disciplined pattern is (1) deploy the FeatureView in shadow mode (present but not consumed by the model), (2) sweep the offline history in chunks — typically 6 hours at a time, from newest to oldest — at a bounded write rate under the online-store budget (35k writes/s under a 40k DDB budget), (3) use conditional writes (&lt;code&gt;ConditionExpression="attribute_not_exists OR event_ts &amp;lt; :ts"&lt;/code&gt;) so the backfill is safe to run alongside the live sync path, (4) verify completion with a random-sample check on 1000 entities before enabling the feature, and (5) dark-launch for one week before letting the model consume it. Feast's &lt;code&gt;materialize --start-date --end-date&lt;/code&gt; command is the canonical implementation; the same command handles both first-launch backfill and cold-restart re-hydration. Skipping the throttle and firing a naive backfill at full write budget will saturate the online store and cascade into serving latency.&lt;/p&gt;

&lt;h3&gt;
  
  
  Feast vs Tecton vs Databricks Feature Store — which do I pick in 2026?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Feast&lt;/strong&gt; is open-source, BYO-infra, works with any offline (Snowflake, BigQuery, Iceberg, Delta, Redshift) and any online (Redis, DynamoDB, Cassandra, Postgres) store. Pick it when the team wants full control, has existing infra, and can absorb the ops overhead of running the materialization and streaming jobs. &lt;strong&gt;Tecton&lt;/strong&gt; is managed, includes streaming compute (Spark Structured Streaming under the covers), and has the most opinionated end-to-end experience — pick it when the team wants batteries-included and is happy with a vendor-managed control plane. &lt;strong&gt;Databricks Feature Store&lt;/strong&gt; is native to Delta Lake and Unity Catalog; pick it when the team already runs Databricks for everything else and the offline store is Delta. All three model the offline/online split; the differences are in ops burden, integration surface, and whether you want to run the streaming compute yourself. In 2026 the mainstream shape is Feast on Iceberg + DynamoDB for teams that want portability, or Databricks FS for teams already all-in on Databricks. Choose based on the existing stack, not the marketing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practice on PipeCode
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Drill the &lt;a href="https://pipecode.ai/explore/practice/topic/streaming" rel="noopener noreferrer"&gt;streaming practice library →&lt;/a&gt; for the sync-path, streaming-push, and out-of-order-event problems senior interviewers love.&lt;/li&gt;
&lt;li&gt;Rehearse on the &lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;ETL practice library →&lt;/a&gt; for the batch materialize, backfill sweep, and PIT-join pipelines that motivate the two-store architecture in the first place.&lt;/li&gt;
&lt;li&gt;Sharpen the tuning axis with the &lt;a href="https://pipecode.ai/explore/practice/topic/optimization" rel="noopener noreferrer"&gt;optimization practice library →&lt;/a&gt; for the freshness-budget, drift-monitoring, and online-store-sizing problems.&lt;/li&gt;
&lt;li&gt;Stack the prerequisites against PipeCode's broader 450+ data-engineering catalogue to anchor the feature-store + freshness intuition against real graded inputs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Lock in feature-store muscle memory&lt;/h3&gt;

&lt;p&gt;Feature-store docs explain APIs. PipeCode drills explain the decision — when streaming push beats batch materialize, when the freshness SLA drives the sync path, when the backfill wave saturates the online store. &lt;a href="https://pipecode.ai/" rel="noopener noreferrer"&gt;Pipecode.ai&lt;/a&gt; is Leetcode for Data Engineering — pattern-first practice tuned for the production trade-offs senior ML data engineers actually face.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/streaming" rel="noopener noreferrer"&gt;Practice streaming problems →&lt;/a&gt;&lt;br&gt;
&lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;Practice ETL problems →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>sql</category>
      <category>interview</category>
      <category>dataengineering</category>
    </item>
  </channel>
</rss>
