Chapter 17 · Physical Warehouse Design: Schemas, Tables, Constraints, Partitioning, Clustering, and Distribution

Create a Physical Design from Workload Evidence Rather Than Copying Generic Partition Recommendations

Turn a fixed query corpus, plan evidence, scan accounting, constraint tests, and skew measurements into a physical-design decision record with assumptions, rollback, and migration cautions.

Intermediate → Advanced155–185 minutesPhysical-design acceptance labPython 3.13.5 · SQLite 3.46.1 · local/syntheticLast reviewed: September 2026

Learning outcomes

AtlasMart now has multiple pieces of evidence: date files prune Q1/Q2, a customer-led access path helps Q3, full scan Q4 remains a full scan, customer hashing is skewed, constraints reject two negative cases, and every layout returns the same query results. The final task is to turn those facts into a reversible decision—not to declare one universal best layout.

01

Build a physical-design decision from a fixed query corpus, exact scan accounting, plan evidence, skew, constraints, lifecycle needs, and rollback cost.

02

Reject physical choices that improve one benchmark while changing logical results or breaking history/recovery contracts.

03

Distinguish deterministic acceptance evidence from environment-specific latency observations.

04

Document which recommendations are portable intents and which are SQLite/local-harness implementations.

05

Produce a reproducible artifact that future engine migrations can challenge with new measurements rather than inherited 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. A physical-design decision record starts with invariants

The first line of the decision record is logical_model_changed = false. That matters more than the chosen index. The accepted production controls remain 9/7/11/740/450/290. The benchmark is a deterministic 21,600-row expansion with SHA-256 c1ec45150cb017bed341ec1de5f728fbf8df533e77d7b7f1f73418c5f7cc0a4a.

The decision record then states the workload, environment, access structures, partition simulation, skew, negative tests, known limitations, and rollback. This makes “why did we choose this?” answerable months later.

2. Decision matrix for this exact local corpus

Evidence Observation Decision consequence
Q1 daily GMV Date layout: 1 file / 23,520 bytes; heap plan SCAN; indexed plan SEARCH Date-oriented physical pruning/access is justified for date-bounded work.
Q2 7-day + product Date layout: 7 files / 164,640 bytes Daily grouping still removes most corpus bytes; product access may need secondary clustering/index.
Q3 customer window Customer hash: 1 bucket / 319,200 bytes; SQLite customer/date SEARCH Customer-led access is useful, but hash skew must be evaluated before distributed adoption.
Q4 full GMV All layouts scan complete 1,411,200-byte file corpus Do not claim partitions remove work for logically full scans.
Hash skew 16,800 vs 4,800 nonempty rows; ratio 3.5 Reject balance assumptions; test real cardinality/frequency before distribution.
Tiny partitions 240 estimated date×product files, ~5,880 bytes average Avoid this extra granularity in the local fixture; no evidence it pays for itself.
Constraints Zero-quantity and orphan-customer inserts rejected Keep executable integrity checks in harness; retest target-engine enforcement.

3. Recommended local design and its portability

For this harness, keep the atomic fact_sales logical model; group file-oriented processing by order_date; use SQLite composite indexes (order_date, product_id) and (customer_id, order_date) for selective row-store access; do not claim distributed placement because SQLite is single-node. These are not product-neutral commands. The portable intents are: prune date-bounded scans, support selective customer/product access, preserve backfill/retention isolation, avoid extreme file counts, and measure skew before distributed placement.

4. Benchmark timing is supporting—not deciding—evidence

Generation-time medians/p95s over 15 warm-cache executions were recorded in benchmark_report.json. Q1 improved from 0.6883/0.7405 ms heap median/p95 to 0.0585/0.1017 ms indexed; Q3 from 0.9392/1.0185 ms to 0.3155/0.3513 ms. Q4 was essentially unchanged/slightly slower with indexes. These are real measurements from the generation environment, but the dataset is tiny, cache was not flushed, and concurrency is one. They must not be promoted into production capacity claims.

The deterministic evidence—the same query results, plan class, exact file bytes, partition counts, and constraint outcomes—is more reproducible across reruns of this local lab.

5. Controlled failure: optimize one dashboard and externalize the cost

If the team chooses a high-cardinality partition key because one dashboard becomes fast, it may create thousands/millions of objects, slow planning/listing, complicate retention/backfills, and make batch loads expensive. If it chooses one distribution key for a single join, other joins can shuffle more data. Physical design is a portfolio tradeoff across representative workloads, load paths, backfills, maintenance, concurrency, and cost.

The repair is a benchmark corpus with query weights and acceptance thresholds. Add a new physical structure only when evidence shows it improves the target workload without violating correctness and with an acceptable maintenance/rollback burden.

6. Full reproducible AtlasMart lab

Create ch17_lab.py with the following standard-library-only script. It deletes/recreates only atlasmart_ch17_lab/, writes two SQLite databases and three file-layout variants, runs the fixed query corpus, records measured timings, performs negative constraint tests, calculates exact scan bytes/skew, verifies layout-equivalent results, and writes two JSON reports.

ch17_lab.py
from pathlib import Pathimport sqlite3, json, hashlib, csv, shutil, statistics, timefrom datetime import date, timedeltaROOT = Path('/mnt/data/atlasmart_ch17_lab')if ROOT.exists():    shutil.rmtree(ROOT)ROOT.mkdir()for d in ['db','layouts/date','layouts/month','layouts/hash_customer','reports']:    (ROOT/d).mkdir(parents=True, exist_ok=True)PYTHON_VERSION = __import__('sys').version.split()[0]SQLITE_VERSION = sqlite3.sqlite_versionTZ = 'UTC'# Chapter 16 accepted control state: 9 paid lines, 7 orders, 11 units, 740 GMV, 450 cost, 290 gross profit.seed = [    # order_id, line_no, customer_id, product_id, store_id, qty, revenue, cost    ('O0999',1,'C003','P300','SALES',1,80,50),    ('O1000',1,'C001','P200','WEB',1,75,45),    ('O1001',1,'C001','P100','WEB',2,100,60),    ('O1001',2,'C001','P200','WEB',1,25,15),    ('O1002',1,'C002','P300','MOBILE',1,195,125),    ('O1003',1,'C001','P400','WEB',1,100,60),    ('O1003',2,'C001','P200','WEB',2,55,30),    ('O1005',1,'C004','P100','MOBILE',1,50,30),    ('O1006',1,'C002','P100','WEB',1,60,35),]assert sum(x[5] for x in seed)==11assert sum(x[6] for x in seed)==740assert sum(x[7] for x in seed)==450assert len({x[0] for x in seed})==7# Deterministic benchmark expansion; does not replace continuity controls.rows=[]start=date(2026,7,24)replicas=40for day_i in range(60):    d=(start+timedelta(days=day_i)).isoformat()    for rep in range(replicas):        for idx,s in enumerate(seed):            order_id,line_no,cust,prod,store,qty,rev,cost=s            # Stable unique order id per benchmark replica/day while preserving dimensional distribution.            oid=f'{order_id}-D{day_i:02d}-R{rep:02d}'            rows.append((d,oid,line_no,cust,prod,store,qty,rev,cost,rev-cost))assert len(rows)==60*replicas*len(seed)logical_checksum=hashlib.sha256('\n'.join('|'.join(map(str,r)) for r in rows).encode()).hexdigest()schema='''PRAGMA foreign_keys=ON;CREATE TABLE dim_customer(customer_id TEXT PRIMARY KEY, customer_name TEXT NOT NULL);CREATE TABLE dim_product(product_id TEXT PRIMARY KEY, product_name TEXT NOT NULL);CREATE TABLE dim_store(store_id TEXT PRIMARY KEY, store_name TEXT NOT NULL);CREATE TABLE fact_sales(  order_date TEXT NOT NULL,  order_id TEXT NOT NULL,  line_no INTEGER NOT NULL,  customer_id TEXT NOT NULL REFERENCES dim_customer(customer_id),  product_id TEXT NOT NULL REFERENCES dim_product(product_id),  store_id TEXT NOT NULL REFERENCES dim_store(store_id),  quantity INTEGER NOT NULL CHECK(quantity > 0),  revenue_usd INTEGER NOT NULL CHECK(revenue_usd >= 0),  cost_usd INTEGER NOT NULL CHECK(cost_usd >= 0),  gross_profit_usd INTEGER NOT NULL,  PRIMARY KEY(order_id,line_no));'''def make_db(path, indexed=False):    cx=sqlite3.connect(path)    cx.executescript(schema)    cx.executemany('INSERT INTO dim_customer VALUES (?,?)',[(f'C{i:03d}',f'Customer {i:03d}') for i in range(1,6)])    cx.executemany('INSERT INTO dim_product VALUES (?,?)',[(f'P{i}',f'Product {i}') for i in (100,200,300,400)])    cx.executemany('INSERT INTO dim_store VALUES (?,?)',[('WEB','Web'),('MOBILE','Mobile'),('SALES','Sales')])    cx.executemany('INSERT INTO fact_sales VALUES (?,?,?,?,?,?,?,?,?,?)',rows)    if indexed:        cx.execute('CREATE INDEX idx_fact_sales_date_product ON fact_sales(order_date, product_id)')        cx.execute('CREATE INDEX idx_fact_sales_customer_date ON fact_sales(customer_id, order_date)')    cx.commit(); cx.close()heap=ROOT/'db/atlasmart_heap.db'; indexed=ROOT/'db/atlasmart_indexed.db'make_db(heap,False); make_db(indexed,True)queries={ 'Q1_daily_gmv':("SELECT SUM(revenue_usd) FROM fact_sales WHERE order_date=?",('2026-09-21',)), 'Q2_date_product':("SELECT SUM(revenue_usd) FROM fact_sales WHERE order_date BETWEEN ? AND ? AND product_id=?",('2026-09-15','2026-09-21','P100')), 'Q3_customer_window':("SELECT SUM(revenue_usd) FROM fact_sales WHERE customer_id=? AND order_date BETWEEN ? AND ?",('C002','2026-09-01','2026-09-21')), 'Q4_full_gmv':("SELECT SUM(revenue_usd) FROM fact_sales",()),}def explain(db,sql,args):    cx=sqlite3.connect(db)    plan=[' | '.join(map(str,r)) for r in cx.execute('EXPLAIN QUERY PLAN '+sql,args)]    val=cx.execute(sql,args).fetchone()[0]    cx.close(); return plan,valplans={}for name,(sql,args) in queries.items():    hp,hv=explain(heap,sql,args); ip,iv=explain(indexed,sql,args)    assert hv==iv    plans[name]={'result':hv,'heap_plan':hp,'indexed_plan':ip}# Local timing is real but environment-specific; use medians across 15 executions after 3 warmups.def timing(db,sql,args):    cx=sqlite3.connect(db)    for _ in range(3): cx.execute(sql,args).fetchone()    vals=[]    for _ in range(15):        t=time.perf_counter_ns(); cx.execute(sql,args).fetchone(); vals.append((time.perf_counter_ns()-t)/1e6)    cx.close()    return {'median_ms':round(statistics.median(vals),4),'p95_ms':round(sorted(vals)[int(0.95*(len(vals)-1))],4),'runs':15}timings={n:{'heap':timing(heap,*q),'indexed':timing(indexed,*q)} for n,q in queries.items()}# Physical file-layout simulator: write canonical rows as compact JSONL.def dump_partition(path, part_rows):    with path.open('w',encoding='utf-8',newline='\n') as f:        for r in part_rows:            f.write(json.dumps(r,separators=(',',':'))+'\n')    return path.stat().st_sizeby_date={}by_month={}for r in rows:    by_date.setdefault(r[0],[]).append(r)    by_month.setdefault(r[0][:7],[]).append(r)date_manifest={d:dump_partition(ROOT/f'layouts/date/{d}.jsonl',rs) for d,rs in sorted(by_date.items())}month_manifest={m:dump_partition(ROOT/f'layouts/month/{m}.jsonl',rs) for m,rs in sorted(by_month.items())}# Stable hash buckets by customer_id, to simulate distribution/co-location reasoning.def bucket(key,n=8):    return int(hashlib.sha256(key.encode()).hexdigest()[:16],16)%nhash_rows={i:[] for i in range(8)}for r in rows: hash_rows[bucket(r[3])].append(r)hash_manifest={str(i):dump_partition(ROOT/f'layouts/hash_customer/bucket={i}.jsonl',rs) for i,rs in hash_rows.items()}# Dimension co-location using same hash rule.customers=[f'C{i:03d}' for i in range(1,6)]customer_buckets={c:bucket(c) for c in customers}row_bucket_counts={str(i):len(hash_rows[i]) for i in range(8)}nonempty=[v for v in row_bucket_counts.values() if v]skew_ratio=round(max(nonempty)/min(nonempty),3)# Scan accounting for Q1-Q4 on the file layouts. No database-engine pruning is claimed.def bytes_sum(man): return sum(man.values())all_date_bytes=bytes_sum(date_manifest); all_month_bytes=bytes_sum(month_manifest); all_hash_bytes=bytes_sum(hash_manifest)def dates_between(a,b):    return [d for d in date_manifest if a<=d<=b]scan={ 'Q1_daily_gmv':{   'date':{'files':1,'bytes':date_manifest['2026-09-21']},   'month':{'files':1,'bytes':month_manifest['2026-09']},   'hash_customer':{'files':8,'bytes':all_hash_bytes}, }, 'Q2_date_product':{   'date':{'files':len(dates_between('2026-09-15','2026-09-21')),'bytes':sum(date_manifest[d] for d in dates_between('2026-09-15','2026-09-21'))},   'month':{'files':1,'bytes':month_manifest['2026-09']},   'hash_customer':{'files':8,'bytes':all_hash_bytes}, }, 'Q3_customer_window':{   'date':{'files':len(dates_between('2026-09-01','2026-09-21')),'bytes':sum(date_manifest[d] for d in dates_between('2026-09-01','2026-09-21'))},   'month':{'files':1,'bytes':month_manifest['2026-09']},   'hash_customer':{'files':1,'bytes':hash_manifest[str(customer_buckets['C002'])]}, }, 'Q4_full_gmv':{   'date':{'files':len(date_manifest),'bytes':all_date_bytes},   'month':{'files':len(month_manifest),'bytes':all_month_bytes},   'hash_customer':{'files':8,'bytes':all_hash_bytes}, },}# Same query results from partition files, proving layout did not change logical semantics.def scan_json_files(paths,predicate):    gmv=0    for p in paths:        for line in p.read_text(encoding='utf-8').splitlines():            r=json.loads(line)            if predicate(r): gmv+=r[7]    return gmvq1_file=scan_json_files([ROOT/'layouts/date/2026-09-21.jsonl'],lambda r:r[0]=='2026-09-21')q2_file=scan_json_files([ROOT/f'layouts/date/{d}.jsonl' for d in dates_between('2026-09-15','2026-09-21')],lambda r:'2026-09-15'<=r[0]<='2026-09-21' and r[4]=='P100')q3_bucket=customer_buckets['C002']q3_file=scan_json_files([ROOT/f'layouts/hash_customer/bucket={q3_bucket}.jsonl'],lambda r:r[3]=='C002' and '2026-09-01'<=r[0]<='2026-09-21')q4_file=scan_json_files([ROOT/f'layouts/date/{d}.jsonl' for d in date_manifest],lambda r:True)assert q1_file==plans['Q1_daily_gmv']['result']assert q2_file==plans['Q2_date_product']['result']assert q3_file==plans['Q3_customer_window']['result']assert q4_file==plans['Q4_full_gmv']['result']# Constraints: valid insert succeeds; invalid quantity and orphan customer are rejected.cx=sqlite3.connect(indexed); cx.execute('PRAGMA foreign_keys=ON')constraint_results={}for name,sql,args in [ ('negative_quantity','INSERT INTO fact_sales VALUES (?,?,?,?,?,?,?,?,?,?)',('2026-09-22','BAD-QTY',1,'C001','P100','WEB',0,10,5,5)), ('orphan_customer','INSERT INTO fact_sales VALUES (?,?,?,?,?,?,?,?,?,?)',('2026-09-22','BAD-CUST',1,'C999','P100','WEB',1,10,5,5)),]:    try:        cx.execute(sql,args); cx.rollback(); constraint_results[name]='NOT_REJECTED'    except sqlite3.IntegrityError as e:        constraint_results[name]='REJECTED: '+str(e)cx.close()assert all(v.startswith('REJECTED') for v in constraint_results.values())# Tiny/high-cardinality partition calculation (do not create 21,600 files).tiny_partition_count=len(date_manifest)*4  # date x producthigh_card_partition_count=len(rows)  # one per row/order-line surrogate-like keyavg_date_file=round(all_date_bytes/len(date_manifest),1)avg_tiny_est=round(all_date_bytes/tiny_partition_count,1)# Decision record based on this specific corpus.decision={ 'logical_model_changed':False, 'benchmark_rows':len(rows), 'continuity_controls':{'paid_lines':9,'paid_orders':7,'units':11,'gmv_usd':740,'cost_usd':450,'gross_profit_usd':290}, 'recommended_local_layout':{   'fact_sales_processing_partition':'order_date (logical/file partition in this harness)',   'access_path':'SQLite composite indexes on (order_date, product_id) and (customer_id, order_date)',   'distribution':'not applicable in single-node SQLite; simulate customer hash only for skew/co-location reasoning' }, 'reasons':[   'Q1/Q2 date predicates prune the deterministic file corpus substantially.',   'Q3 benefits from customer-led access; hash-by-customer illustrates possible co-location but is poor for date-only pruning.',   'Q4 is a full scan by definition, so partitioning does not remove scan work for that query.',   'Date-by-product tiny partitions multiply file count without changing grain.',   'Distribution/clustering semantics are engine-specific and require re-benchmarking after migration.' ], 'rollback':'Keep logical schema/grain unchanged; remove/rebuild indexes or rematerialize file layout from canonical rows if evidence changes.'}report={ 'runtime':{'python':PYTHON_VERSION,'sqlite':SQLITE_VERSION,'timezone':TZ,'mode':'local/synthetic','cache_note':'SQLite page/OS cache not flushed; plans and byte accounting are primary evidence; timings are environment-specific.'}, 'logical_checksum':logical_checksum, 'rows':len(rows), 'plans':plans, 'timings_ms':timings, 'file_bytes':{'date_total':all_date_bytes,'month_total':all_month_bytes,'hash_total':all_hash_bytes,'date_files':len(date_manifest),'month_files':len(month_manifest),'hash_files':8,'avg_date_file':avg_date_file,'estimated_date_product_files':tiny_partition_count,'estimated_avg_date_product_bytes':avg_tiny_est,'high_cardinality_partition_count':high_card_partition_count}, 'scan':scan, 'hash_distribution':{'customer_buckets':customer_buckets,'row_bucket_counts':row_bucket_counts,'nonempty_skew_ratio':skew_ratio}, 'constraint_tests':constraint_results, 'query_results':{k:v['result'] for k,v in plans.items()}, 'layout_equivalence':{'Q1':q1_file,'Q2':q2_file,'Q3':q3_file,'Q4':q4_file}, 'decision':decision,}(ROOT/'reports/benchmark_report.json').write_text(json.dumps(report,indent=2),encoding='utf-8')(ROOT/'reports/physical_design_decision.json').write_text(json.dumps(decision,indent=2),encoding='utf-8')# Human-readable concise output used by lesson verification.print(f'runtime: Python {PYTHON_VERSION} / SQLite {SQLITE_VERSION} / {TZ}')print('continuity controls: (9, 7, 11, 740, 450, 290)')print('benchmark rows:',len(rows))print('query results:',{k:v['result'] for k,v in plans.items()})print('Q1 heap plan:',plans['Q1_daily_gmv']['heap_plan'])print('Q1 indexed plan:',plans['Q1_daily_gmv']['indexed_plan'])print('Q3 indexed plan:',plans['Q3_customer_window']['indexed_plan'])print('date files/bytes:',len(date_manifest),all_date_bytes)print('month files/bytes:',len(month_manifest),all_month_bytes)print('hash bucket counts:',row_bucket_counts,'skew_ratio:',skew_ratio)print('Q1 date scan:',scan['Q1_daily_gmv']['date'])print('Q1 hash scan:',scan['Q1_daily_gmv']['hash_customer'])print('Q3 hash scan:',scan['Q3_customer_window']['hash_customer'])print('tiny partition estimate:',tiny_partition_count,'files avg_bytes',avg_tiny_est)print('high-cardinality partition estimate:',high_card_partition_count,'files')print('constraints:',constraint_results)print('layout equivalence:',report['layout_equivalence'])print('decision logical_model_changed:',decision['logical_model_changed'])print('logical checksum:',logical_checksum)print('cleanup: rm -rf atlasmart_ch17_lab')

Expected acceptance summary

expected_output.txt
runtime: Python 3.13.5 / SQLite 3.46.1 / UTCcontinuity controls: (9, 7, 11, 740, 450, 290)benchmark rows: 21600query results: {'Q1_daily_gmv': 29600, 'Q2_date_product': 58800, 'Q3_customer_window': 214200, 'Q4_full_gmv': 1776000}Q1 heap plan: ['3 | 0 | 0 | SCAN fact_sales']Q1 indexed plan: ['4 | 0 | 0 | SEARCH fact_sales USING INDEX idx_fact_sales_date_product (order_date=?)']date files/bytes: 60 1411200month files/bytes: 3 1411200hash bucket counts: {'0': 4800, '1': 0, '2': 0, '3': 16800, '4': 0, '5': 0, '6': 0, '7': 0} skew_ratio: 3.5Q1 date scan: {'files': 1, 'bytes': 23520}Q3 hash scan: {'files': 1, 'bytes': 319200}tiny partition estimate: 240 files avg_bytes 5880.0high-cardinality partition estimate: 21600 fileslayout equivalence: {'Q1': 29600, 'Q2': 58800, 'Q3': 214200, 'Q4': 1776000}decision logical_model_changed: Falselogical checksum: c1ec45150cb017bed341ec1de5f728fbf8df533e77d7b7f1f73418c5f7cc0a4a

Verification checklist

  • Open reports/benchmark_report.json and confirm runtime, query corpus, plans, timings, scan bytes, skew, constraints, and layout-equivalent results are present.
  • Confirm Q1/Q3 are SCAN in the heap database and SEARCH in the indexed database.
  • Confirm every file-layout query result equals the SQLite result.
  • Confirm both negative constraint cases begin with REJECTED:.
  • Confirm physical_design_decision.json says logical_model_changed: false.
  • Rerun the script; deterministic rows/checksum/scan bytes must remain stable. Timings may vary.

Cleanup/reset

cleanup.sh
rm -rf atlasmart_ch17_lab# Windows PowerShell equivalent:# Remove-Item -Recurse -Force .tlasmart_ch17_lab

Cleanup is appropriate for the synthetic lab. Production benchmark evidence and decision records should be versioned/retained according to engineering governance because they explain why a physical layout exists.

7. Rollback and migration

The safest physical optimization is reversible around a stable logical model. If evidence changes, drop/rebuild indexes or rematerialize file layout from canonical inputs; do not rewrite metric semantics. Before migration, rerun the corpus on the target engine and replace SQLite/file-simulator observations with the target's plan, scan bytes, shuffle/skew, concurrency, load/backfill, and cost evidence.

8. Bridge to Chapter 18

Chapter 17 treated file bytes largely as uncompressed physical accounting and SQLite as a row-store harness. Chapter 18 explains why columnar storage changes the economics: projection, encoding/compression, predicate/statistics skipping, and vectorized execution can reduce bytes and CPU even when the logical model is unchanged. The same discipline continues—measure the target engine rather than assume a storage format is automatically fast.

Knowledge check

Check your understanding

  1. What is the first invariant in the decision record?
  2. Which deterministic evidence is stronger than the local timing numbers?
  3. What local layout is recommended for date-bounded file processing?
  4. Why is customer hash not adopted as a universal layout?
  5. What is the rollback if the workload changes?
Review the answers

1. logical_model_changed = false; physical optimization must preserve the dimensional/metric contract.

2. Query-result equivalence, SCAN/SEARCH plan class, exact files/bytes, constraint outcomes, row/bucket counts, and checksum.

3. Order-date grouping, because Q1/Q2 prune substantially in this specific corpus and it aligns with retention/backfill reasoning.

4. It helps a known-customer query but is poor for date-only queries and shows 3.5× nonempty skew in this fixture.

5. Keep the logical model; remove/rebuild physical indexes/layout and rerun the corpus rather than migrating business semantics.

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.