Skip to main content

Choosing a Partition Key, and the Small-Files Problem

Level 6: Level 6: Cloud & Data Engineering Foundationsmedium25 minpartition key selectioncardinalitysmall-files problemover-partitioningconditional aggregation

Partition on the low-cardinality column you filter on (usually date). Partition on a high-cardinality key and you shred the table into millions of tiny files, where per-file overhead dominates.

Choosing the partition key is the real skill

Partitioning is easy to do and easy to do badly. The interview question is never "can you partition," it is "what would you partition on, and what goes wrong if you choose wrong." Three rules cover most of it.

Rule 1: partition on the column you filter by, at low-to-moderate cardinality. The whole payoff is pruning, and you only prune on the partition key, so partition on what queries actually filter on. That is almost always a date (dt), sometimes a region or a category. A date has naturally low cardinality (365 values a year) and matches how analysts slice data.

Table
Partition on a low-cardinality column you filter on. High-cardinality keys shred the table into tiny files.
candidate keycardinalityresult
dt (date)low (365 per year)great: matches the common filter, healthy file sizes
countrylowgood: prunes region queries
user_idvery highbad: millions of tiny partitions, small-files problem
event_iduniqueterrible: one partition per row
Partition on a low-cardinality column you filter on. High-cardinality keys shred the table into tiny files.

Rule 2: avoid the small-files problem. Partition on a high-cardinality column like user_id and you get a huge number of tiny partitions, each with tiny files. This is pathological, because every file carries fixed overhead: the engine opens it, reads its footer, and tracks its metadata, and object stores start refusing requests when you hammer them (S3 returns "please reduce your request rate"). AWS measured reading 100,000 small files taking 11.5 seconds against 4.3 seconds for the same data in one file. The rule of thumb is to aim for files around 128 MB, not kilobytes. Many small files are slower than a few big ones, even for the same total bytes. (This 128 MB is a whole-file target, which is a different knob from the 128 MB row-group default inside a Parquet file from the last module: a single file holds one or more row groups.)

Rule 3: do not over-partition. Partitioning on a column you rarely filter on, or one with thousands of values, multiplies partition metadata and shreds your files without buying any pruning. If a would-be key is high cardinality, partition coarser (by month instead of day, or by a bucket of the value) or bucket it instead (next lesson).

Spotting the problem with SQL

A partition catalog that records each partition's file count and total bytes lets you compute the average file size and flag the partitions suffering the small-files problem. The exercises do exactly that: find the partitions whose average file is far below the 128 MB target, and measure how many excess tiny files they add.

Common mistake: partitioning on a high-cardinality column like user_id. It creates a partition per value, which shreds the table into millions of tiny files where per-file overhead dominates and object stores start throttling requests.

Interview nuance: "you have millions of tiny files and queries are slow, what happened" is a classic. The answer is over-partitioning on a high-cardinality key: too many partitions means too many tiny files, and per-file overhead plus metadata dominate. The fix is a coarser partition key (or bucketing) and compaction into roughly 128 MB files.

On a real platform this differs. Real engines expose file counts and sizes through catalog stats or a storage inventory, and compaction jobs (or Iceberg and Delta "optimize") merge small files for you. The partition_files table here gives you the file count and bytes per partition so you can compute the average file size the same way an audit query would.

Sample data for this example
CREATE TABLE partition_files (
  partition_key TEXT,
  file_count    INTEGER,
  total_bytes   INTEGER
);
INSERT INTO partition_files (partition_key, file_count, total_bytes) VALUES
  ('dt=2026-01-01', 4,   520000000),   -- 130 MB avg file, healthy
  ('dt=2026-01-02', 4,   540000000),   -- 135 MB avg file, healthy
  ('dt=2026-01-03', 40,   80000000),   -- 2 MB avg file, small-files problem
  ('dt=2026-01-04', 200, 100000000),   -- 0.5 MB avg file, severe small-files
  ('dt=2026-01-05', 3,   450000000);   -- 150 MB avg file, healthy
Worked example (SQL)
-- The layout: how many files each partition split into (watch the outliers).
SELECT partition_key, file_count,
       ROUND(total_bytes / 1000000.0, 0) AS total_mb
FROM partition_files
ORDER BY file_count DESC, partition_key;

Apply

Your turn

The task this lesson builds to.

Write a query that returns each partition's average file size in MB and a health flag, as (partition_key, file_count, avg_file_mb, health), worst (smallest average file) first, over partition_files(partition_key, file_count, total_bytes).

The average file size is total bytes divided by file count (treat 1 MB as 1,000,000 bytes). health is 'small-files' when the average file is under 128 MB and 'ok' otherwise. Round avg_file_mb to 2 decimals, ordered by it ascending.

3 hints and 1 automated check are waiting in the workspace.

Practice

Make it stick

A second problem on the same idea, plus 4 bonus drills.

Write a query that returns only the partitions suffering the small-files problem, with how many files they carry beyond a healthy 4-file target, as (partition_key, file_count, excess_files), most excess first, over the same partition_files table.

A partition has the problem when its average file (total bytes divided by file count) is under 128 MB. A healthy partition of roughly half a gigabyte holds about four 128 MB files, so excess_files is its file count minus that 4-file target. Order by excess_files descending.

3 hints and 1 automated check are waiting in the workspace.