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.

Intermediate → Advanced135–165 minutesPartition pruning/skew labPython 3.13.5 · SQLite 3.46.1 · local/syntheticLast reviewed: September 2026

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.

01

Distinguish partitioning from fact grain and from secondary clustering/index access paths.

02

Calculate exact files/bytes scanned for date, coarser range, and stable-hash layouts in the deterministic AtlasMart corpus.

03

Explain partition pruning, retention, backfill scope, tiny partitions, and high-cardinality partition failure modes.

04

Measure hash skew before recommending distribution-like layouts.

05

Choose partition shape from workload predicates and lifecycle operations rather than from generic date-partition folklore.

Chapter 17 continuity contract

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.

Execution and guarantee boundary

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.

inspect_scan_bytes.py
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

verify_partition_evidence.py
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

  1. What does partition pruning require?
  2. Why does customer hashing not help Q1?
  3. How many daily files does Q2 scan in the fixture?
  4. Why is date × product over-partitioned in this small fixture?
  5. 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

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.