Incremental Sort Plans #
A paginated feed — ORDER BY account_id, created_at DESC LIMIT 50 — has an index on account_id but not on (account_id, created_at). Before PostgreSQL 13 the only options were to sort everything or add an index. Now the plan shows Incremental Sort with Presorted Key: account_id, reads a few hundred rows, and returns in under a millisecond. For one large account, the same plan takes six seconds.
Sort spills, work_mem sizing and unexpected sorts are covered in sort and hash node analysis. This page covers the sort node that exploits partial order.
The Algorithmic Condition #
Incremental Sort (PostgreSQL 13+, enable_incremental_sort) applies when:
- the required order is
(a, b, …), and - the input already arrives ordered by a leading prefix
(a)— from an index scan, a merge join, or a lower sort.
Instead of buffering all input, it processes rows in runs that share prefix values. Two modes:
- Full-sort mode. Collect a small batch of rows (at least 32), possibly spanning several prefix groups, and sort the batch on all keys. Efficient when groups are tiny.
- Pre-sorted mode. When a batch reveals a large group with a single prefix value, collect that whole group and sort it on the remaining keys only.
Benefits over a full Sort:
- Memory is proportional to the largest group, not the whole input.
- Early output: rows can be emitted after each group, so a
LIMITstops the scan early.
The planner costs it assuming groups of average size from n_distinct of the prefix column. That assumption is exactly what fails on skewed prefixes — the same class of estimate error covered in statistics and selectivity estimation. Deciding whether the planner should see presorted input at all is part of debugging unexpected sort operations.
Annotated EXPLAIN Evidence #
Typical accounts:
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM activity
WHERE account_id BETWEEN 1000 AND 2000
ORDER BY account_id, created_at DESC
LIMIT 50;
Limit (actual time=0.09..0.71 rows=50 loops=1)
-> Incremental Sort (actual time=0.09..0.69 rows=50 loops=1)
Sort Key: account_id, created_at DESC
Presorted Key: account_id -- input already ordered by this
Full-sort Groups: 2 Sort Method: quicksort Average Memory: 30kB Peak Memory: 30kB
-- two small batches covered the first 50 rows
-> Index Scan using activity_account_id_idx on activity (actual rows=71 loops=1)
Index Cond: ((account_id >= 1000) AND (account_id <= 2000))
Execution Time: 0.76 ms
A skewed prefix — account 1000 is an integration account with 3.8 million rows:
Limit (actual time=6104.2..6104.3 rows=50 loops=1)
-> Incremental Sort (actual time=6104.2..6104.3 rows=50 loops=1)
Sort Key: account_id, created_at DESC
Presorted Key: account_id
Full-sort Groups: 1 Sort Method: quicksort Average Memory: 28kB Peak Memory: 28kB
Pre-sorted Groups: 1 Sort Method: external merge Average Disk: 412208kB Peak Disk: 412208kB
-- one pre-sorted group of 3.8M rows, sorted on disk before the first row came out
-> Index Scan using activity_account_id_idx on activity (actual rows=3804211 loops=1)
Execution Time: 6110.8 ms
Fields to read: Presorted Key (which prefix is exploited), the two group counts, and Peak Memory / Peak Disk for the pre-sorted mode — a large peak there means one group dominated.
Step-by-Step Resolution #
-
Find
Incremental Sortand compareSort KeywithPresorted Keyto see which columns remain to be sorted. -
Read the group statistics. Many full-sort groups with small peak memory is healthy. A pre-sorted group with large memory or disk means a skewed prefix.
-
Check prefix skew.
SELECT account_id, count(*) FROM activity GROUP BY account_id ORDER BY count(*) DESC LIMIT 5; -
Test alternatives.
BEGIN; SET LOCAL enable_incremental_sort = off; EXPLAIN (ANALYZE, BUFFERS) <query>; ROLLBACK; -
Give the planner full order when the query is hot. An index matching the entire sort key removes sorting entirely and makes
LIMITstop after 50 rows regardless of skew:CREATE INDEX CONCURRENTLY activity_account_created_idx ON activity (account_id, created_at DESC);Column order and direction rules are in descending and NULLS ordering in B-tree indexes.
-
Size memory for the largest group if the incremental plan is otherwise right; the pre-sorted mode obeys
work_memlike any sort.
Before and After #
-- BEFORE: index on (account_id) only, skewed account
Incremental Sort Pre-sorted Groups: 1 Sort Method: external merge Peak Disk: 412208kB Execution Time: 6110.8 ms
-- AFTER: index on (account_id, created_at DESC)
Limit → Index Scan using activity_account_created_idx (actual rows=50) Execution Time: 0.41 ms
The full-key index is worth its write cost only for hot paginated queries; for occasional reports over typical accounts, the incremental plan on the single-column index was already fast, and the skewed account can be handled by its own query path.
Common Pitfalls #
Reading Incremental Sort as “no sort cost”. It still sorts; it just sorts in pieces. Diagnostic signal: Pre-sorted Groups with disk usage. Fix: check group sizes before assuming the node is cheap.
Average estimates on skewed prefixes. The planner’s group size assumption hides the one huge group. Diagnostic signal: fast plans for most parameter values and one pathological value. Fix: a full-key index, or split the hot value into its own query.
Disabling the feature globally. Many paginated and merge-join plans benefit. Diagnostic signal: new full Sort nodes across unrelated queries. Fix: scope enable_incremental_sort = off to the transaction.
Mismatched sort directions. An index on account_id with ORDER BY account_id DESC, created_at still provides a presorted prefix via a backward scan, but mixed directions on the remaining keys cannot come from a single-direction index. Diagnostic signal: incremental sort persisting after adding an index with the wrong direction. Fix: match the index directions to the ORDER BY.
Frequently Asked Questions #
When does PostgreSQL use Incremental Sort?
When the input is already sorted by a leading prefix of the sort keys and sorting each prefix group separately is estimated cheaper than a full sort. Available from PostgreSQL 13, controlled by enable_incremental_sort.
What are Full-sort Groups and Pre-sorted Groups?
Full-sort mode sorts small batches on all keys; pre-sorted mode sorts one large same-prefix group on the remaining keys. EXPLAIN reports counts and memory or disk for each.
Why is Incremental Sort sometimes slower than a full sort?
The estimate assumes average group sizes. A skewed prefix value creates one huge group that is effectively a full sort, plus overhead, and the LIMIT benefit disappears.
Related #
- Sort and Hash Node Analysis — parent guide: sort methods, spills and memory
- Debugging Unexpected Sort Operations — sibling: why a sort appeared at all
- Multicolumn Index Column Ordering — building the index that supplies the full order