Skip to content
About
Main content

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 of every the query might touch. A full scan touches all of them, so planning time grows with the partition count, and a that serves many 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

At 20 plans a second, how many planning slots does an unfiltered read over a million partitions keep busy?

Run it

  1. 1Install uv once. It fetches Python and the lab's packages for you.

    Terminal
    curl -LsSf https://astral.sh/uv/install.sh | sh
  2. 2Get the script. It is the whole lab: read it, then download it.

    planning-time-versus-partition-count.py42 linesDownload
    View 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)
    
  3. 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
Knobs
Knob · defaultWhat it changes
--counts1000, 10,000, 100,000, 1,000,000 Partition counts.
--target20 Target plans per second.
--repeats5 Timings per query (median).

Our run

Planning time against partition countApple M2 · 8 cores · 8 GB
Full scanDate filter, indexed
Data table
Planning time against partition count
PartitionsFull scanDate filter, indexed
1,0000.4390.016
10K4.690.016
100K48.70.016
1M4850.015
macOS 26.6.2 · Python 3.13.3 · run 2026-10-20
Output · uv run planning-time-versus-partition-count.py
    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.69

What 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.