Both names say “minimum,” and both parameters do set one: a scan the planner expects to read less than min_parallel_table_scan_size of table (default 8MB) or less than min_parallel_index_scan_size of index (default 512kB) is not considered for a parallel plan. That is the documented half. The other half is that each value is also the bottom rung of the ladder the planner climbs to decide how many workers to ask for, so moving the minimum moves every rung above it.
Both are user context, so any session can change them, and they can be set per role or per database. The range is 0 to 715,827,882 blocks (a third of INT_MAX, about 5.3TB, picked so that the tripling loop described below cannot overflow). The unit is blocks, which matters if you leave the unit off:
1 SET min_parallel_table_scan_size = 8;
2 SHOW min_parallel_table_scan_size;
3 min_parallel_table_scan_size
4 ------------------------------
5 64kB
Write '8MB'. PostgreSQL 9.6 had a single parameter, min_parallel_relation_size. Version 10 added parallel index scans and parallel bitmap heap scans, and split it into these two, since one number could no longer describe a table scan and an index scan at once.
Also the ruler
The max_parallel_workers_per_gather post covered the ladder: one worker at 8MB, one more each time the table triples, the total capped by max_parallel_workers_per_gather. The 8MB in that sentence is this parameter. The planner starts at min_parallel_table_scan_size, multiplies by three until it passes the size of the table, and counts the steps. This is a 116MB table on 18.6, with max_parallel_workers_per_gather at 16 and the parallel cost parameters zeroed so that only the ladder is deciding:
min_parallel_table_scan_size |
Workers planned |
|---|---|
1MB |
5 |
8MB |
3 |
64MB |
1 |
128MB |
none (serial plan) |
0 |
9 |
Look at the last row. 0 does not mean “no minimum, and otherwise as before.” The code floors the starting point at one block, so the ladder runs 8kB, 24kB, 72kB and onward, and a 116MB table is eight triplings up from there. The PostgreSQL regression tests set it to 0, along with zeroed parallel costs, to get parallel plans out of toy tables. In a test that is fine. On a server where someone has also raised max_parallel_workers_per_gather, it hands seven workers to a 10MB table. On my two-core test instance, a filtered count(*) over a 9.6MB table took about 10ms serially, and 33 to 37ms with the seven workers it was given at 0 and a per-gather limit of 8.
The comment over that loop in the source admits that “we need something here for now.” It was written in 2015, for 9.6. It is still there in 19 beta 4.
Eligible is not chosen
Clearing the minimum makes a parallel plan eligible. Whether it gets chosen is a cost comparison, mostly against parallel_setup_cost (default 1000), and that comparison is about rows and the work done per row, where the minimum is about pages. I built tables of increasing size and ran a count(*) with one cheap filter against each at default settings. With rows 81 to a page, the plan stayed serial until about 204,000 rows, which is 20MB; the minimum had been satisfied since 8MB and it made no difference. With two-integer rows at 226 to a page, cost was ready to say yes at about 200,000 rows again (I had to lower the minimum to see it), which is 7MB, so at defaults that table went parallel at exactly 8MB and the minimum was the thing deciding. Make the per-row work expensive enough and the same happens to wide rows: a 6.9MB table with five function calls in its WHERE clause stayed serial at the default and took a worker at 4MB.
So the default sits about where the cost model starts agreeing for narrow rows and cheap filters, and for ordinary rows it is satisfied well before cost is.
Lowering the minimum does make smaller tables go parallel, but mostly through the ladder: lower rungs offer more workers, and more workers make the parallel plan cheaper on paper. A 5.2MB table of two-integer rows stayed serial with the minimum at 4MB, where one worker was on offer, and got a two-worker plan at 0.
Two things skip the calculation. The parallel_workers storage parameter replaces it. And the minimum applies only to a table scanned on its own account. The children of an append (partitions, inheritance children, the arms of a UNION ALL) get a parallel path whatever their size, on the theory that many small pieces add up. Sixteen 2MB hash partitions still produced a Parallel Append with the minimum at 1GB; the same rows in a single table went serial.
A table under the minimum can also still appear inside a parallel plan. It just can’t be the scan that gets divided among the workers. A 48kB lookup table joined to a large one is read in full by each worker, which is what you want.
The index one
min_parallel_index_scan_size is compared with the planner’s estimate of how many index pages the scan will read. The size of the index does not enter into it. On a five-million-row table with a 107MB primary key, a range of 20,000 ids (an estimated 54 index pages) stayed serial, and a range of 30,000 (an estimated 80) got one worker; the default is 64 pages. The same tripling ladder runs up from there.
A plain index scan has to clear both minimums, and it gets the smaller of the two worker counts. The table check uses a pessimistic estimate of heap pages (it assumes the rows are scattered across the table), so it is rarely the one that says no; the 30,000-id range read under 3MB of heap and still cleared an 8MB minimum. An index-only scan is checked against the index minimum alone. The code skips the heap check deliberately, since an index-only scan may fetch almost no heap. A range covering four million of those rows planned four workers as a plain index scan and five as an index-only scan. A parallel bitmap heap scan is the reverse: only the table minimum applies, against estimated heap pages, and the index parameter is ignored, because the bitmap index scan underneath is not the parallel part.
B-tree is the only access method with parallel index scans, so as far as queries go, this is a B-tree parameter.
Not only the planner
CREATE INDEX reads the session’s min_parallel_table_scan_size and climbs the same ladder to decide how many workers to request, as the max_parallel_maintenance_workers post described. Raising it in postgresql.conf to keep small queries serial therefore also makes index builds serial on every table below the new value. With it at 1GB, B-tree and BRIN builds on a 482MB table (BRIN builds have been parallel since 17) went from requesting a parallel worker to building serially. That includes every index built by a pg_restore.
VACUUM uses min_parallel_index_scan_size to decide which indexes are large enough to hand to a parallel worker, and there it is the index’s real size on disk, with no estimate involved. Three 456kB indexes got no workers; after SET min_parallel_index_scan_size = 0 in the same session, the next VACUUM (PARALLEL 2) launched two.
PostgreSQL 19 adds two more readers. Parallel TID range scans are checked against the size of the whole table, whatever the range. Parallel autovacuum applies the same index cutoff as manual VACUUM.
Leave both at their defaults. If parallel plans are showing up on queries too small to benefit, the parameter for that is parallel_setup_cost. If large tables are getting too few workers, it is max_parallel_workers_per_gather. The one job only this parameter can do is stretch the ladder: if you raised the per-gather limit to 8 for the sake of the big tables and every 1GB table now asks for five workers, min_parallel_table_scan_size = '64MB' brings that down to three without touching the cap. It also makes every scan and every index build under 64MB serial, so set it on the roles that got the higher limit and leave postgresql.conf alone. And if you find either parameter at 0 outside a test suite, someone copied it out of one.