Chapter 17 · Physical Warehouse Design: Schemas, Tables, Constraints, Partitioning, Clustering, and Distribution
Partitioning by Date/Range/Hash: Pruning, Retention, Backfill, Small Partitions, and Skew
Compare date, range-like month, and stable-hash partitioning using exact file/byte accounting; diagnose over-partitioning, high-cardinality partitions, retention/backfill effects, and skew.
Learning outcomes
AtlasMart frequently asks for one day, seven days, or a customer-window slice. “Partition by date” sounds obvious, but physical partitioning creates files/metadata, affects backfills and retention, and may be useless for customer-only access. Lesson 3 turns those tradeoffs into deterministic file and byte counts rather than slogans.
Distinguish partitioning from fact grain and from secondary clustering/index access paths.
Calculate exact files/bytes scanned for date, coarser range, and stable-hash layouts in the deterministic AtlasMart corpus.
Explain partition pruning, retention, backfill scope, tiny partitions, and high-cardinality partition failure modes.
Measure hash skew before recommending distribution-like layouts.
Choose partition shape from workload predicates and lifecycle operations rather than from generic date-partition folklore.
Physical design must not redefine the business model. The accepted Chapter 16 current controls remain 9 paid order-line facts, 7 paid orders, 11 units, 740 USD paid GMV, 450 USD cost-at-sale, and 290 USD gross profit. The benchmark harness expands those nine rows deterministically to 21,600 synthetic rows only to make physical-access differences observable. Benchmark-scale totals are test data, not new production controls. Grain, conformed dimension meaning, SCD history policy, metric formulas, source contracts, and Chapter 15–16 recovery semantics remain unchanged.
The mandatory lab is local, synthetic, and free.
Generation-time validation ran with Python 3.13.5 and SQLite
3.46.1 in UTC. SQLite is used only as a row-store/access-path
harness; it has no native cloud-warehouse clustering or
distributed distribution keys. Date/month/hash file layouts
are deterministic JSONL simulations used to calculate files
and bytes that a dispatcher would scan. The lab never claims
those numbers are Snowflake, BigQuery, Redshift, Databricks,
ClickHouse, or DuckDB behavior.
EXPLAIN QUERY PLAN evidence is SQLite-specific
and its output format is documented as unstable across SQLite
versions; the lessons interpret only the observed SCAN/SEARCH
mechanism, not the literal formatting.
1. Partitioning is a physical grouping of rows/files—not the business grain
A partition key decides which physical partition receives a row. Pruning is the ability to exclude partitions from a query because partition metadata proves they cannot match the predicate. Range partitioning groups contiguous key ranges; hash partitioning maps keys through a deterministic hash to buckets; date partitioning is a common range specialization.
One order line remains one fact row whether its file is grouped by day, month, hash bucket, or not partitioned. Confusing partition key with grain leads teams to drop necessary columns or duplicate rows merely to fit a storage layout.
2. The benchmark layouts contain identical rows and bytes
The harness writes exactly the same 21,600 canonical rows three ways: 60 daily JSONL files, 3 monthly JSONL files, and 8 customer-hash bucket files. Each layout totals 1,411,200 bytes because JSON serialization is identical; only grouping changes.
| Query | Layout | Files scanned | Bytes scanned | Interpretation |
|---|---|---|---|---|
| Q1 daily GMV | Date files | 1 / 60 | 23,520 | Best fit for one-day predicate in this corpus |
| Q1 daily GMV | Month files | 1 / 3 | 493,920 | Coarser range; fewer files but more bytes |
| Q1 daily GMV | Customer-hash files | 8 / 8 | 1,411,200 | Hashing by customer cannot prune a date-only query |
| Q2 7-day + product | Date files | 7 / 60 | 164,640 | Date predicate prunes; product still filtered inside files |
| Q3 customer + 21 days | Date files | 21 / 60 | 493,920 | Date pruning only |
| Q3 customer + 21 days | Customer hash | 1 / 8 | 319,200 | Customer equality identifies one bucket |
| Q4 full GMV | Any complete layout | all | 1,411,200 | A full logical scan still requires all benchmark rows |
These are exact local file sizes, not estimates of a columnar engine. A real columnar format would add file/footer metadata, compression, row groups, statistics, and projection/predicate pushdown effects. Chapter 18 explores those mechanisms.
3. Date partitioning wins Q1/Q2 here because the predicates prove date bounds
Q1 reads 23,520 bytes from one daily file instead of all 1,411,200 bytes. Q2 reads seven daily files totaling 164,640 bytes. The monthly layout must scan 493,920 bytes for either query because both dates fall in September.
This is not “daily is always better.” Daily files increase object/partition count and can become tiny at low volume. Month partitions can be preferable when daily volume is small or most queries scan month-sized ranges. Partition choice is an economic/workload decision.
import jsonfrom pathlib import Pathr=json.loads(Path("atlasmart_ch17_lab/reports/benchmark_report.json").read_text())for q in ["Q1_daily_gmv","Q2_date_product","Q3_customer_window","Q4_full_gmv"]: print(q, r["scan"][q])
4. Hash partitioning can localize equality access and still be terrible for another query
Q3 knows customer C002, so the stable customer hash identifies one bucket containing 319,200 bytes. But Q1 is date-only; the same customer-hash layout must inspect all eight buckets and all 1,411,200 bytes. Partitioning helps only when the query predicates align with the partition metadata.
There is another problem: skew. The five synthetic customers map into only three nonempty buckets; one holds 16,800 rows and another 4,800, giving a nonempty max/min ratio of 3.5. A distributed engine could turn that skew into uneven worker load. Hash functions distribute keys, not business frequency.
| Customer | Stable bucket |
|---|---|
| C001 | 3 |
| C002 | 0 |
| C003 | 3 |
| C004 | 3 |
| C005 | 5 |
5. Tiny and high-cardinality partitions multiply metadata and maintenance
If AtlasMart partitioned by date × product, the 60-day/4-product benchmark would create 240 partitions averaging about 5,880 bytes each in this JSONL model. If it partitioned by a unique order-line identity, it would create 21,600 partitions—one per row. Neither changes the logical grain, but both create disproportionate file/metadata/listing/retention/backfill overhead.
Most analytical engines have their own minimum useful partition/file sizes and metadata costs; do not import a universal threshold. Measure actual object counts, partition pruning, file sizes, metadata planning time, and compaction/maintenance behavior in the target platform.
6. Retention and backfill are first-class partition requirements
Partitioning can simplify lifecycle operations: deleting an expired date range or rebuilding one damaged day can be safer than row-by-row operations. But that benefit exists only when retention/backfill scope aligns with the partition key. Customer-hash buckets are awkward for “rebuild 2026-09-20” because every bucket may contain that date.
Chapter 16 already taught that backfill scope is semantic. Chapter 17 adds a physical question: can the storage layout isolate the affected history without rewriting unrelated data?
7. Controlled failure: date partition every table
A tiny customer dimension with one current row per customer gains little from daily partitioning and may become harder to query/maintain. A large event fact can benefit because most queries/retention policies are date-bounded. Apply partitioning to the workload and lifecycle, not to the word “warehouse.”
8. Hands-on acceptance
import jsonfrom pathlib import Pathr=json.loads(Path("atlasmart_ch17_lab/reports/benchmark_report.json").read_text())assert r["file_bytes"]["date_files"] == 60assert r["scan"]["Q1_daily_gmv"]["date"] == {"files": 1, "bytes": 23520}assert r["scan"]["Q1_daily_gmv"]["hash_customer"]["bytes"] == 1411200assert r["file_bytes"]["estimated_date_product_files"] == 240assert r["file_bytes"]["high_cardinality_partition_count"] == 21600print("partition evidence accepted")
9. Production judgment and bridge
Choose partitions when they remove meaningful work or simplify lifecycle operations enough to offset object/metadata/write/backfill complexity. Then test skew and small-file/partition behavior at realistic scale. Lesson 4 adds clustering/sort/index access paths and distribution/co-location, mechanisms that can complement partitions but are frequently confused with them.
Knowledge check
Check your understanding
- What does partition pruning require?
- Why does customer hashing not help Q1?
- How many daily files does Q2 scan in the fixture?
- Why is date × product over-partitioned in this small fixture?
- Does a hash function guarantee balanced distributed work?
Review the answers
1. Metadata or partition boundaries that prove some partitions cannot satisfy the query predicate.
2. Q1 filters only by date, so no customer bucket can be excluded.
3. Seven files, totaling 164,640 bytes.
4. It creates 240 very small partitions (~5,880 bytes average) without evidence that the extra metadata/maintenance cost is justified.
5. No. Business-frequency skew can concentrate rows even when key hashing is deterministic.
Authoritative references
- SQLite — EXPLAIN QUERY PLANOfficial documentation for interpreting SQLite SCAN/SEARCH plan evidence; the output format is explicitly not an application contract.
- SQLite — Query PlanningBackground on index-assisted row access in the local row-store harness.
-
SQLite — Foreign Key SupportOfficial constraint behavior; the fixture enables
PRAGMA foreign_keys=ONexplicitly per connection. - SQLite — CREATE TABLEDDL reference for PRIMARY KEY, CHECK, and REFERENCES used in the local constraint test.
- Python — sqlite3Standard-library interface used to keep the mandatory lab dependency-free.
- Python — hashlibStable SHA-256 hashing for benchmark checksums and deterministic hash-bucket assignment.
- Kimball Group — Dimensional Modeling TechniquesBackground for preserving declared fact/dimension grain while changing the physical implementation.