SQL SQL In Depth
0%
lesson scd-type-2 dimensional-modeling history-tracking

Slowly Changing Dimensions: SCD Type 0, 1, 2 (Effective Dates), 3 & 6

7 min read Module 3.2: Dimensional Modeling & Analytical Schema Design Updated Oct 26, 2026
📄 Cheat Sheet
Slowly Changing Dimensions: SCD Type 0, 1, 2 (Effective Dates), 3 & 6 — Database Systems & SQL Architecture Lecture
Strange Loop (Martin Kleppmann) Slowly Changing Dimensions: SCD Type 0, 1, 2 (Effective Dates), 3 & 6 — Database Systems & SQL Architecture Lecture Watch on YouTube ↗

Builds first-principles mastery of Slowly Changing Dimensions: SCD Type 0, 1, 2 (Effective Dates), 3 & 6 with concrete query execution trees, storage page layouts, and production tuning trade-offs.

0:00 / 3:52

1. First-Principles Mental Model & Relational Mechanics

Every SQL statement in Slowly Changing Dimensions: SCD Type 0, 1, 2 (Effective Dates), 3 & 6 compiles down to a deterministic tree of relational algebra operators acting on physical pages in the buffer pool. At the logical layer, Preserving point-in-time historical truth when customer tiers or territories change using SCD Type 2 (valid_from, valid_to, is_current) rows. Rather than mutating global variables imperatively, declarative SQL specifies the mathematical properties of the desired output relation while delegating physical access path selection to the cost-based query optimizer.

When the database engine processes slowly-changing-dimensions-scd within Dimensional Modeling & Analytical Schema Design, it enforces strict invariants over tuple domains, attribute visibility, and I/O page budgets. Understanding the boundary between logical relational semantics (what rows qualify) and physical execution (whether the engine performs a sequential heap scan, a B+Tree range seek, or an in-memory hash probe) is the defining skill of a Staff Database Engineer.

Formal Relational & Cost Invariant — Slowly Changing Dimensions: SCD Type 0, 1, 2 (Effective Dates), 3 & 6

Let relation RR contain NN tuples stored across BB physical 8KB pages (B≈⌈N/tuples_per_page⌉B \approx \lceil N / \text{tuples\_per\_page} \rceil). Evaluating Slowly Changing Dimensions: SCD Type 0, 1, 2 (Effective Dates), 3 & 6 maintains the relational closure and I/O cost bound:

CostI/O(R)=Bseq⋅cseq+log⁡F(N)⋅crandom,σθ(πA(R))⊆Dom(A)\text{Cost}_{\text{I/O}}(R) = B_{\text{seq}} \cdot c_{\text{seq}} + \log_{F}(N) \cdot c_{\text{random}}, \quad \sigma_{\theta}(\pi_{A}(R)) \subseteq \text{Dom}(A)

Where F≈250–500F \approx 250\text{--}500 is the B+Tree page fanout, σθ\sigma_{\theta} denotes Three-Valued Logic predicate selection over scd-type-2, and πA\pi_{A} projects the target attribute schema without violating domain constraints.

2. Database Table Schema, Execution Pipeline & Visual Architecture

Tracing how Slowly Changing Dimensions: SCD Type 0, 1, 2 (Effective Dates), 3 & 6 flows through the parser, catalog binder, cost-based optimizer, and physical executor reveals exactly where latency and cardinality bugs originate:

  • Stage 1 — Lexical Parsing & Catalog Binding (scd-type-2): The SQL parser builds an Abstract Syntax Tree (AST) and resolves every relation, column identifier, and data type against system catalog tables (such as pg_class, pg_attribute, or sqlite_master).
  • Stage 2 — Logical Rewrite & Algebraic Simplification (dimensional-modeling): The planner normalizes predicates, pushes selective filters as close to the storage scan as possible, and verifies whether Three-Valued NULL semantics permit subquery unnesting or join elimination.
  • Stage 3 — Cost Estimation & Physical Operator Selection (history-tracking): Using histogram boundaries, Most Common Values (MCV), and Number of Distinct Values (NDV), the optimizer selects the minimum-cost physical plan and streams tuples through the execution pipeline.
Slowly Changing Dimensions: SCD Type 0, 1, 2 (Effec... — Partitioned Table & Sliding Window Frame Module M08 · PARTITION BY dept_id ORDER BY salary_usd DESC · Frame Overlay (scd-type-2) TABLE employees (Partitioned Tuple Stream) PK emp_id dept_id name salary_usd RANK / SUM OVER 2 2 (Storage) Vikramaditya $255,000 #1 · $255k 4 2 (Storage) Mateo Silva $215,000 #2 · $470k [CURRENT ROW] 5 2 (Storage) Aisha Patel $198,000 #3 · $668k 3 3 (Planner) Hannah L. $250,000 #1 · $250k (New Partition) WINDOW FRAME INVARIANTS • Preserves all N input tuples • Frame: UNBOUNDED PRECEDING AND CURRENT ROW • Peer ties handled by ROWS vs RANGE vs GROUPS • Evaluated at Step 5 (pre-SELECT)
erDiagram CUSTOMERS ||--o{ ORDERS : "places (customer_id)" ORDERS ||--|{ ORDER_ITEMS : "contains (order_id)" PRODUCTS ||--o{ ORDER_ITEMS : "referenced_by (product_id)" CUSTOMERS { int customer_id PK text email UK text name text tier text country } ORDERS { int order_id PK int customer_id FK text status int total_cents text discount_code } ORDER_ITEMS { int order_item_id PK int order_id FK int product_id FK int quantity int unit_price_cents }

3. Interactive Table-Level & Storage Engine Simulator

Use the interactive simulator below to step through Slowly Changing Dimensions: SCD Type 0, 1, 2 (Effective Dates), 3 & 6 at the tuple and page level. Observe how input rows, intermediate filter/join states, NULL badges, and physical page reads evolve at each step:

Star Schema Fact/Dimension Grain & Slowly Changing Dimension (SCD Type 2) Merger
Kimball SCD Type 2

Step through SCD Type 2 historical row expiration (valid_to = t_change, is_current = false), surrogate key lookup, and point-in-time AS OF fact joins.

Step 1 / 6
Custom Input:
Timeline

Invariant & State

Relation initialized with 4 tuples across schema (cust_sk, cust_nk, tier, valid_from, valid_to, is_current) — preset: Customer Tier Upgrade (Pro → Enterprise) SCD Type 2 Split.
🔗 Live Pointer & State Transition Graph — Star Schema Fact/Dimension Grain & Slowly Changing Dimension (SCD Type 2) Merger Active Pointer Jump/Transition Matched/Optimal
Tuple # 1001:… [0] 1048:… [1] 1102:… [2] 1015:… [3]
📋 Star Schema Fact/Dimension Grain & Slowly Changing Dimension (SCD Type 2) Merger — Initial Relation State 4 row(s)
cust_skcust_nktiervalid_fromvalid_tois_currentState
1001C-50Starter2025-01-012025-06-15false (Closed)CURSOR START
1048C-50Pro2025-06-152026-02-01false (Closed)
1102C-50Enterprise2026-02-019999-12-31true (Active)
1015C-88Pro2025-03-109999-12-31true (Active)
[0]1001:StarterTuple #0
[1]1048:Pro
[2]1102:Enterprise
[3]1015:Pro
Input Tuples
4
Columns
6
Preset Mode
scd2
Pipeline Stage
1. Initial Scan
[Step 1] Initialized Star Schema Fact/Dimension Grain & Slowly Changing Dimension (SCD Type 2) Merger (Customer Tier Upgrade (Pro → Enterprise) SCD Type 2 Split) with 4 base rows.

4. Live WASM SQL Lab, Clause-by-Clause Visualizer & Automated Test Checker

Execute, edit, and test the production SQL query below directly in your browser using the embedded WebAssembly SQLite 3 engine. Click ▶ Run Query to execute, 🔍 Step Clauses to inspect the live intermediate table after FROM/JOIN, WHERE, GROUP BY, HAVING, WINDOW, SELECT, and ORDER BY, or ✓ Check SQL Tests to run automated relational assertions against the reference result:

SQL · WASM13 lines
-- Dataset: org_hr | SCD Type 2 Effective-Date Compensation History & Raise Deltas
SELECT
  e.name AS employee_name,
  e.title,
  sh.old_salary_usd,
  sh.new_salary_usd,
  (sh.new_salary_usd - sh.old_salary_usd) AS raise_delta_usd,
  ROUND(((sh.new_salary_usd - sh.old_salary_usd) * 100.0) / sh.old_salary_usd, 1) AS raise_pct,
  sh.effective_date
FROM salary_history AS sh
INNER JOIN employees AS e
  ON e.emp_id = sh.emp_id
ORDER BY sh.effective_date ASC, raise_delta_usd DESC;

5. Cross-Engine Dialect & Cost Comparison Matrix

While the SQL ISO standard defines the logical behavior of Slowly Changing Dimensions: SCD Type 0, 1, 2 (Effective Dates), 3 & 6, production storage engines differ in how they execute, lock, and index the underlying tuples:

Database Engine Primary Storage & Access Mechanism I/O & CPU Complexity Production Caveat for Slowly Changing Dimensions: SCD Type 0, 1, 2 (Effective Dates), 3 & 6
PostgreSQL 17 Heap Tuple Store (ctid) + B-Tree / GIN / BRIN + MVCC (xmin/xmax) O(log⁡BN)O(\log_B N) seek / O(N)O(N) scan Requires ANALYZE statistics and Visibility Map bits for Index-Only Scans.
SQLite 3.46 (WASM) B+Tree Table (rowid clustered) + Covering Secondary B-Trees O(log⁡BN)O(\log_B N) B+Tree traversal Runs in-process with zero network RTT; inspect EXPLAIN QUERY PLAN for USE TEMP B-TREE.
MySQL 8.4 (InnoDB) Clustered Primary Key B+Tree + Undo Log Rollback Segments O(log⁡BN)O(\log_B N) PK / $2 \times O(\log_B N)$ Secondary Secondary indexes store the Primary Key value; wide PKs amplify secondary index footprint.
DuckDB / ClickHouse Columnar Vectorized Chunks + Zone Maps (Min/Max Skipping) O(N/SIMD_Width)O(N / \text{SIMD\_Width}) vectorized scan Excels at wide analytical scans and aggregations; avoid single-row OLTP point mutations.

6. Live Verification, Catalog Introspection & Execution Plan Lab (SQL)

To verify how a production database engine executes Slowly Changing Dimensions: SCD Type 0, 1, 2 (Effective Dates), 3 & 6 across edge-case rows, run or modify the second live SQL verification query below. Inspect how GROUP BY buckets, window partitions, and Three-Valued NULL guards behave in real time:

SQL · WASM26 lines
-- Dataset: org_hr | Live Verification & Departmental Window Audit (slowly-changing-dimensions-scd)
WITH dept_rollup AS (
  SELECT
    d.dept_id,
    d.name AS dept_name,
    COUNT(e.emp_id) AS staff_count,
    ROUND(AVG(e.salary_usd), 0) AS avg_salary_usd,
    MAX(e.salary_usd) AS top_salary_usd,
    SUM(e.salary_usd) AS total_payroll_usd,
    d.annual_budget_usd
  FROM departments AS d
  LEFT JOIN employees AS e
    ON e.dept_id = d.dept_id
  GROUP BY d.dept_id, d.name, d.annual_budget_usd
)
SELECT
  dept_id,
  dept_name,
  staff_count,
  avg_salary_usd,
  top_salary_usd,
  total_payroll_usd,
  ROUND((total_payroll_usd * 100.0) / annual_budget_usd, 1) AS budget_utilization_pct
FROM dept_rollup
WHERE staff_count >= 1
ORDER BY total_payroll_usd DESC;

7. Production Failure Modes, Anti-Patterns & Staff Tuning Checklist

Production Pitfall & SEV-1 Outage Pattern

The most common production failure in Slowly Changing Dimensions: SCD Type 0, 1, 2 (Effective Dates), 3 & 6 occurs when a query passes functional unit tests on a 50-row local fixture but degrades into an O(N2)O(N^2) loop, an unindexed full-table scan, or a silent NULL data-drop once table cardinality crosses 10710^7 rows. Always verify both NULL boundary rows and physical buffer reads before deploying to production.

  • Verify Execution Plans Early: Never guess what the planner will do—run EXPLAIN QUERY PLAN (SQLite) or EXPLAIN (ANALYZE, BUFFERS) (PostgreSQL) and confirm that estimated row counts match actual row counts within 2×2\times.
  • Guard Against Three-Valued NULL Traps: Audit every NOT IN, outer-join WHERE filter, and aggregate AVG/COUNT(col) expression for nullable columns; prefer NOT EXISTS and explicit COALESCE defaults.
  • Keep Predicates SARGable: Never wrap an indexed column in a function, arithmetic expression, or implicit type cast on the left-hand side of a comparison operator.
  • Bound Memory & Lock Scope: Ensure sorting, hashing, and batch mutations operate within bounded memory budgets (work_mem) and hold row/table locks for the shortest possible duration.
Check yourself Question 1 of 4
Concept Check Slowly Changing Dimensions: SCD Type 0, 1, 2 (Effective Dates), 3 & 6 — Core Relational Invariant recall

Which statement accurately characterizes the primary mechanism and guarantee of Slowly Changing Dimensions: SCD Type 0, 1, 2 (Effective Dates), 3 & 6?

Interactive 3-Step Guided SQL Lab & Step-by-Step Table Visualizer (PostgreSQL 18 WASM & SQLite 3 WASM) 3 Guided Steps + Progressive Hints

Slowly Changing Dimensions: SCD Type 0, 1, 2 (Effective Dates), 3 & 6 — Interactive Guided SQL Lab

Preserving point-in-time historical truth when customer tiers or territories change using SCD Type 2 (valid_from, valid_to, is_current) rows.

⏱ Target Time: O(N) scan / O(log_B N) indexed 💾 Auxiliary Space: O(1) streaming tuple state 🧭 Pattern: Dimensional Modeling & Analytical Schema Design 🗄️ Dataset: org_hr
01 Phase 1 · Base Relation & SARGable Predicate
Base Relation Scan & SARGable Filter (org_hr)
Establish the base relation scan in org_hr for Slowly Changing Dimensions: SCD Type 0, 1, 2 (Effective Dates), 3 & 6 with a SARGable predicate and explicit Three-Valued Logic NULL handling.
Run or edit query.sql to inspect the base table tuples, verify SARGable index filtering in the WHERE clause, and step through the live Table Visualizer.
💡 Progressive SQL Hint (Step 1)
SELECT emp_id, name, title, dept_id, manager_id, salary_usd, hired_at FROM employees WHERE salary_usd >= 170000 ORDER BY salary_usd DESC LIMIT 10;
02 Phase 2 · Relational Join, Aggregation & 3VL Guard
Relational Join, Grouping & NULL-Safe Aggregation
Combine normalized tables via explicit JOIN conditions and fold group buckets or window partitions without losing NULL boundary rows.
Run or refine the Step 2 relational join and aggregation query in query.sql, then click 🔍 Step Clauses to inspect each intermediate table.
💡 Progressive SQL Hint (Step 2)
-- Dataset: org_hr | Live Verification & Departmental Window Audit (slowly-changing-dimensions-scd)
WITH dept_rollup AS (
  SELECT
    d.dept_id,
    d.name AS dept_name,
    COUNT(e.emp_id) AS staff_count,
    ROUND(AVG(e.salary_usd), 0) AS avg_salary_usd,
    MAX(e.salary_usd) AS top_salary_usd,
    SUM(e.salary_usd) AS total_payroll_usd,
    d.annual_budget_usd
  FROM departments AS d
  LEFT JOIN employees AS e
    ON e.dept_id = d.dept_id
  GROUP BY d.dept_id, d.name, d.annual_budget_usd
)
SELECT
  dept_id,
  dept_name,
  staff_count,
  avg_salary_usd,
  top_salary_usd,
  total_payroll_usd,
  ROUND((total_payroll_usd * 100.0) / annual_budget_usd, 1) AS budget_utilization_pct
FROM dept_rollup
WHERE staff_count >= 1
ORDER BY total_payroll_usd DESC;
03 Phase 3 · Production Query & Plan Verification
Production Query & Execution Plan Verification (Slowly Changing Dimensions: SCD Type 0, 1, 2 (Effective Dates), 3 & 6)
Verify final column projections, ordering, and physical B+Tree access paths via EXPLAIN QUERY PLAN and automated relational assertions.
Execute the complete production query in query.sql and click ✓ Check SQL Assertions to verify schema alignment, row cardinality, and tuple equivalence.
💡 Progressive SQL Hint (Step 3)
-- Dataset: org_hr | SCD Type 2 Effective-Date Compensation History & Raise Deltas
SELECT
  e.name AS employee_name,
  e.title,
  sh.old_salary_usd,
  sh.new_salary_usd,
  (sh.new_salary_usd - sh.old_salary_usd) AS raise_delta_usd,
  ROUND(((sh.new_salary_usd - sh.old_salary_usd) * 100.0) / sh.old_salary_usd, 1) AS raise_pct,
  sh.effective_date
FROM salary_history AS sh
INNER JOIN employees AS e
  ON e.emp_id = sh.emp_id
ORDER BY sh.effective_date ASC, raise_delta_usd DESC;

Key Takeaways

Slowly Changing Dimensions: SCD Type 0, 1, 2 (Effective Dates), 3 & 6 (slowly-changing-dimensions-scd): Preserving point-in-time historical truth when customer tiers or territories change using SCD Type 2 (valid_from, valid_to, is_current) rows.
Always separate logical SQL semantics (FROM -> WHERE -> GROUP BY -> HAVING -> WINDOW -> SELECT -> ORDER BY -> LIMIT) from the physical execution plan chosen by the cost-based optimizer.
Audit every predicate and join condition for Three-Valued Logic (NULL -> UNKNOWN) edge cases and verify SARGability so B+Tree indexes can perform O(log_B N) range seeks.
Use EXPLAIN QUERY PLAN / EXPLAIN (ANALYZE, BUFFERS) to inspect physical scan operators, join algorithms, and temporary sort/hash spills before shipping queries to production.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue