A PostgreSQL extension that rejects queries which would scan every partition
of a partitioned table without a usable predicate on the partition key. It
prevents accidental full-hierarchy scans caused by missing WHERE/JOIN
conditions on the partition key.
pg_kpart installs a planner_hook. After the standard planner produces a
plan, the plan tree is walked. For each Append / MergeAppend sitting on a
partition hierarchy:
- If the node carries run-time pruning info (
part_prune_info- e.g. a parameterized predicate on the partition key), the key is used and the query is allowed. - Otherwise, if the number of surviving leaf partitions equals the total number of leaf partitions of the queried table, no pruning happened: the partition key was not restricted, and the configured message is raised.
A predicate that prunes only some partitions (e.g. a range on the key spanning a few partitions) leaves fewer leaves than the total and is accepted.
Requires the PostgreSQL server development files (pg_config on PATH).
make
make install # may need sudoThe functional part is a planner hook installed at library load, so the
library must be preloaded. Add to postgresql.conf (cluster-wide):
shared_preload_libraries = 'pg_kpart' # then restart
or per-session / per-database without a restart:
session_preload_libraries = 'pg_kpart'
-- or: ALTER DATABASE mydb SET session_preload_libraries = 'pg_kpart';
The CREATE EXTENSION step is optional and may be used just to register the extension in the pg_catalog.pg_extension table.
| GUC | Default | Description |
|---|---|---|
pg_kpart.enabled |
on |
Master switch. |
pg_kpart.message_level |
error |
error, warning, notice, log, … Use warning to audit before enforcing. |
pg_kpart.min_partitions |
2 |
Only check tables with at least this many leaf partitions. |
pg_kpart.check_superuser |
off |
When off, superusers bypass the check. |
pg_kpart.blacklisted |
(empty) | Comma-separated partitioned tables the check applies to, and their sub-partitions. When set, only these tables are checked and pg_kpart.whitelisted is ignored. Empty = all partitioned tables. |
pg_kpart.whitelisted |
(empty) | Comma-separated partitioned tables exempt from the check, and their sub-partitions. Ignored when pg_kpart.blacklisted is set. |
pg_kpart.nouser_change |
off |
When on, disallow non superuser roles to mofidy the settings of the pg_kpart extension. |
Names may be schema-qualified (schema.table); unqualified names are resolved
through the current search_path. Listing a partitioned table also covers any
sub-partitioned tables beneath it, so a sub-partition queried directly is
matched when one of its ancestors is listed. Membership is decided from the
partitioned table referenced in the query.
-- only police these two tables (and their sub-partitions)
ALTER SYSTEM SET pg_kpart.blacklisted = 'public.measurement, public.orders';
-- alternatively: police everything except a few audit tables
ALTER SYSTEM SET pg_kpart.whitelisted = 'public.audit_log';
SELECT pg_reload_conf();-- roll out in audit mode first
ALTER SYSTEM SET pg_kpart.message_level = 'warning';
SELECT pg_reload_conf();-- partition key is logdate
SELECT * FROM measurement WHERE city_id = 5; -- ERROR: would scan all N partitions
SELECT * FROM measurement WHERE logdate = '2024-03-01'; -- OK (pruned to 1)
SELECT * FROM measurement WHERE logdate >= '2024-06-01'; -- OK (key restricted)
SELECT * FROM measurement WHERE logdate = $1; -- OK (run-time pruning)If table m_2025 is a partition of table measurement and have sub-partioning too,
a full scan on m_2025 will report an error.
SELECT * FROM m_2025 WHERE logdate = '2025-03-01'; -- OK (pruned to 1 sub-partition)
SELECT * FROM m_2025 WHERE logdate >= '2025-01-01' AND logdate < '2026-01-01'; -- ERROR: would scan all N sub-partitionsViolations use the custom SQLSTATE FS001, so applications can trap them:
DO $$
BEGIN
PERFORM count(*) FROM measurement;
EXCEPTION WHEN SQLSTATE 'FS001' THEN
RAISE NOTICE 'caught a full-partition-scan attempt';
END $$;- The check is a guardrail, not a hard barrier.
pg_kpart.enabledandpg_kpart.message_levelareUSERSETGUCs, so any role can runSET pg_kpart.enabled = off(or lower the message level) to bypass enforcement for its own session. Treat it as protection against accidental full scans, not as a security control against deliberate ones. - Enforcement happens at plan time, inside the planner hook. A prepared
statement or cached generic plan is checked only when it is actually
(re)planned; repeated
EXECUTEof an already-cached full-scan plan will not re-trigger the check. - A predicate that happens to match all partitions (e.g.
logdate > '1900-01-01') is treated as a full scan and rejected - it is one, in practice. - The check also covers
UPDATE/DELETEandEXPLAIN(withoutANALYZE), since those go through the planner too. - The check is a guardrail, not a hard barrier.
pg_kpart.enabledandpg_kpart.message_levelareUSERSETGUCs, so any role can runSET pg_kpart.enabled = off(or lower the message level) to bypass enforcement for its own session. Treat it as protection against accidental full scans, not as a security control against deliberate ones. If you want to disallow non superuser roles to mofidy the pg_kpart settings, enablepg_kpart.nouser_change. - Enforcement happens at plan time, inside the planner hook. A prepared
statement or cached generic plan is checked only when it is actually
(re)planned; repeated
EXECUTEof an already-cached full-scan plan will not re-trigger the check. - Tested on PostgreSQL >= 14.
- Gilles Darold
This extension is free software distributed under the PostgreSQL License.
Copyright (c) 2026 HexaCluster Corp