Planning time versus partition count
Measure your machineNeeds Python
Problem
Before a query engine reads any data it plans, and planning a query on a partitioned table means reading the metadata of every partition the query might touch. A full scan touches all of them, so planning time grows with the partition count, and a catalog that serves many planners must be sized for it: by Little's Law, the number of planning requests in flight is the request rate times the planning time.
Create an SQLite table partitions(table_id, day, file_count) and insert 1,000 / 10,000 / 100,000 / 1,000,000 rows for one table. Time SELECT * FROM partitions WHERE table_id = 1 (the work of "planning a full scan") and the same query with a date filter and an index. Then use Little's Law: with the measured planning time W and a target of 20 plans per second, compute the number of slots L = 20 × W.
The question. How does planning time grow with the number of partitions, and how many slots does a catalog need for 20 plans per second?
Predict
Expect planning time to grow linearly with partition count for the unfiltered read.
Run it
1Install uv once. It fetches Python and the lab's packages for you.
Terminal curl -LsSf https://astral.sh/uv/install.sh | sh2Get the script. It is the whole lab: read it, then download it.
View the codeHide the code
planning-time-versus-partition-count.py # /// script # requires-python = ">=3.10" # /// # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT # https://thedataengineering.com/lab-code/LICENSE.txt """Planning time against partition count. An SQLite catalog table partitions(table_id, day, file_count) holds 1,000 to 1,000,000 partitions of one table. Times the unfiltered read of all of them (the work of planning a full scan) and a read with a date filter served by an index, then applies Little's Law: slots L = target plans/s × planning time W. """ import argparse, json, sqlite3, statistics, time p = argparse.ArgumentParser(description=__doc__.splitlines()[0]) p.add_argument("--counts", default="1000,10000,100000,1000000", help="Partition counts.") p.add_argument("--target", type=float, default=20, help="Target plans per second.") p.add_argument("--repeats", type=int, default=5, help="Timings per query (median).") p.add_argument("--json", help="Also write the results to this JSON file.") args = p.parse_args() def timed(c, sql, params=()): ts = [] for _ in range(args.repeats): t = time.perf_counter(); c.execute(sql, params).fetchall(); ts.append(time.perf_counter() - t) return statistics.median(ts) rows = [] for n in map(int, args.counts.split(",")): c = sqlite3.connect(":memory:") c.execute("CREATE TABLE partitions (table_id int, day int, file_count int)") c.executemany("INSERT INTO partitions VALUES (1, ?, ?)", ((d, d % 17 + 1) for d in range(n))) c.execute("CREATE INDEX ON_day ON partitions (table_id, day)") full = timed(c, "SELECT * FROM partitions WHERE table_id = 1") filt = timed(c, "SELECT * FROM partitions WHERE table_id = 1 AND day BETWEEN ? AND ?", (n // 2, n // 2 + 30)) rows.append({"partitions": n, "full_ms": full * 1000, "filtered_ms": filt * 1000, "slots": args.target * full}) for r in rows: print(f"{r['partitions']:>9,} partitions: full-scan planning {r['full_ms']:9.3f} ms filtered {r['filtered_ms']:7.3f} ms slots for {args.target:g} plans/s: {r['slots']:7.2f}") if args.json: with open(args.json, "w") as f: json.dump({"tables": {"planning": rows}}, f)3Run it in the folder you saved it to, with the defaults first. Then change a knob from the table below and run it again.
Terminal uv run planning-time-versus-partition-count.py uv run planning-time-versus-partition-count.py --help # every knob
| Knob · default | What it changes |
|---|---|
| --counts1000, 10,000, 100,000, 1,000,000 | Partition counts. |
| --target20 | Target plans per second. |
| --repeats5 | Timings per query (median). |
Our run
Data table
| Partitions | Full scan | Date filter, indexed |
|---|---|---|
| 1,000 | 0.439 | 0.016 |
| 10K | 4.69 | 0.016 |
| 100K | 48.7 | 0.016 |
| 1M | 485 | 0.015 |
1,000 partitions: full-scan planning 0.439 ms filtered 0.015 ms slots for 20 plans/s: 0.01
10,000 partitions: full-scan planning 4.687 ms filtered 0.016 ms slots for 20 plans/s: 0.09
100,000 partitions: full-scan planning 48.669 ms filtered 0.016 ms slots for 20 plans/s: 0.97
1,000,000 partitions: full-scan planning 484.668 ms filtered 0.015 ms slots for 20 plans/s: 9.69What we found
The unfiltered read grew linearly, from 0.439 ms for 1,000 partitions to 485 ms for a million; the filtered, indexed read stayed at 0.015 ms. At 20 plans per second, a million partitions need 9.69 planning slots busy at all times, against 0.009 for a thousand.
Read it
Partition count is a cost paid on every query that cannot prune it, before any data is read. Fine partitioning helps queries that filter on the partition key and taxes every other query and the catalog that serves them. Little's Law turns that tax into capacity: slots, connections, memory.