PostgreSQL Mastery
PostgreSQL Mastery lessons / Module 31: Extensions

The production extension toolkit

Level 6: Design and Security30 min

Most production PostgreSQL fleets run the same short list of extensions: statistics and plan capture, auditing, scheduling, partition management, online bloat removal, cache warming and index what-ifs. This lesson maps the critical ones, what each solves and how to operate it.

This lesson is interactive. Run every example on a real PostgreSQL 18 server in your browser, read EXPLAIN ANALYZE plans, and take the quiz.
Open the interactive lesson
You will learn to
  • Know the critical extensions by problem area and which need shared_preload_libraries
  • Operate pg_cron, pg_partman, pg_repack, pgstattuple and pg_prewarm safely
  • Test index ideas with hypopg and pin plans with pg_hint_plan only as a last resort

conceptThe concept

ProblemExtensionPreload?
Which queries cost mostpg_stat_statements, pg_stat_monitorYes
Plan of a slow query at the timeauto_explainYes (or per session)
Audit trailpgauditYes
Scheduled jobs in the databasepg_cronYes
Creating and dropping time partitionspg_partmanFor its background worker
Removing bloat without long lockspg_repack, pg_squeezepg_squeeze yes
Measuring bloat preciselypgstattupleNo
Warm cache after restart or failoverpg_prewarmFor autoprewarm
"Would this index help?"hypopgNo
Forcing a planpg_hint_planYes (or LOAD)
Wait-event samplingpg_wait_samplingYes
Domain featuresvector, postgis, timescaledb, citustimescaledb and citus yes

Preloaded extensions allocate shared memory or hooks at startup, so adding them needs a restart, and a missing library file prevents the server from starting. Plan them with upgrades, not during incidents.

whyWhy it matters

These extensions turn common operational chores (nightly jobs, partition rotation, bloat fixes, cache warm-up after failover) into SQL that lives with the database and is visible to every DBA, instead of cron jobs on a single host that disappear with it.

Use caseReal-world scenario

A platform team's standard image preloads pg_stat_statements, auto_explain, pgaudit, pg_cron, pg_prewarm. pg_cron runs partman.run_maintenance_proc() hourly and a nightly VACUUM (ANALYZE) of a staging table; pg_repack is run from a runbook when pgstattuple shows more than 40% dead space; autoprewarm keeps failover warm-up under a minute.

Try itExamples

Run and edit these in the interactive lesson.

What is installed and what is available here
SELECT name, installed_version, default_version,
       installed_version IS DISTINCT FROM default_version AND installed_version IS NOT NULL AS needs_update
FROM pg_available_extensions
WHERE installed_version IS NOT NULL
ORDER BY name;
SELECT setting AS shared_preload_libraries FROM pg_settings WHERE name = 'shared_preload_libraries';
On a real server, run this in every database after a package upgrade: needs_update true means ALTER EXTENSION ... UPDATE has not been run yet.
Estimating bloat without pgstattuple
SELECT relname, n_live_tup, n_dead_tup,
       round(100.0 * n_dead_tup / nullif(n_live_tup + n_dead_tup, 0), 1) AS dead_pct,
       pg_size_pretty(pg_table_size(relid)) AS size, last_autovacuum
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 5;
Statistics give a cheap first view. pgstattuple confirms with exact dead and free space before you schedule a pg_repack.
pg_cron and pg_partman together
-- postgresql.conf: shared_preload_libraries = '...,pg_cron'   cron.database_name = 'shop'
CREATE EXTENSION pg_cron;
CREATE EXTENSION pg_partman SCHEMA partman;

-- daily partitions for events, 7 created ahead, 90 days kept
SELECT partman.create_parent(p_parent_table => 'public.events', p_control => 'at',
                             p_interval => '1 day', p_premake => 7);
UPDATE partman.part_config SET retention = '90 days', retention_keep_table = false
WHERE parent_table = 'public.events';

SELECT cron.schedule('partman-maintenance', '7 * * * *', $$CALL partman.run_maintenance_proc()$$);
SELECT cron.schedule('nightly-analyze', '30 2 * * *', $$VACUUM (ANALYZE) staging.import$$);

-- did it run?
SELECT jobid, status, return_message, start_time FROM cron.job_run_details ORDER BY start_time DESC LIMIT 10;
Measure bloat, then remove it online
CREATE EXTENSION pgstattuple;
SELECT table_len, tuple_percent, dead_tuple_percent, free_percent FROM pgstattuple('orders');
SELECT * FROM pgstattuple_approx('events');              -- cheaper on big tables
SELECT avg_leaf_density, leaf_fragmentation FROM pgstatindex('orders_pkey');

-- shell: rebuild with only a brief exclusive lock at the end
-- pg_repack --dbname=shop --table=public.orders --jobs=2 --wait-timeout=60
Warm the cache after a restart
CREATE EXTENSION pg_prewarm;
SELECT pg_prewarm('orders');                    -- read into shared_buffers now
SELECT pg_prewarm('orders_pkey');

-- automatic: shared_preload_libraries = '...,pg_prewarm'
-- pg_prewarm.autoprewarm = on                  (default when preloaded)
-- pg_prewarm.autoprewarm_interval = 300s       saves the block list every 5 minutes
Index what-ifs and plan hints
CREATE EXTENSION hypopg;
SELECT * FROM hypopg_create_index('CREATE INDEX ON orders (customer_id, created_at)');
EXPLAIN SELECT * FROM orders WHERE customer_id = 42 ORDER BY created_at DESC LIMIT 10;
--  Limit -> Index Scan Backward using "<13457>btree_orders_customer_id_created_at" on orders
SELECT hypopg_reset();

-- pg_hint_plan: last resort, documented and reviewed
LOAD 'pg_hint_plan';
/*+ Leading(c o) HashJoin(c o) IndexScan(o orders_customer_id_idx) */
SELECT ... FROM customers c JOIN orders o ON o.customer_id = c.id WHERE ...;

internalsUnder the hood

pg_cron runs a background worker in one database (cron.database_name) that launches jobs as new connections or background workers and records results in cron.job_run_details. pg_repack builds a new copy of the table while a trigger captures concurrent changes into a log table, replays them, then swaps files under a brief ACCESS EXCLUSIVE lock; it needs a primary key or a unique not-null index. pgstattuple reads every page for exact numbers; pgstattuple_approx skips all-visible pages using the visibility map. autoprewarm periodically writes the list of cached blocks to autoprewarm.blocks and reloads them after start. hypopg creates index definitions only in the session's memory, so plain EXPLAIN (not ANALYZE) can consider them.

Avoid / preferDo this, not that

Avoid
-- incident at 3 a.m.
ALTER SYSTEM SET shared_preload_libraries = 'pg_stat_statements,pg_cron,pgaudit,pg_wait_sampling';
-- restart
Prefer
-- add preload libraries in a planned change, after checking every package is installed on the primary and all replicas

If any listed library file is missing, PostgreSQL refuses to start. A typo in this setting has turned many small changes into outages.

Avoid
/*+ IndexScan(o orders_customer_id_idx) */  -- added to 40 queries after one bad plan
Prefer
-- fix statistics (ANALYZE, extended statistics, per-column targets), indexes or the query first;
-- hint only with a ticket and a review date

Hints freeze a plan that may be wrong once data changes, and they hide the real cause, usually bad estimates. They are for emergencies and documented exceptions.

PitfallsCommon mistakes

  • Installing an extension package on the primary but not on replicas, so a failover or a preload change breaks startup.
  • Running pg_repack on a table without a primary key or with very high write rates without a wait timeout, so it holds up or fails at the swap.
  • Scheduling pg_cron jobs in a database other than cron.database_name and wondering why they never run.
  • Forgetting ALTER EXTENSION ... UPDATE in every database after upgrading extension packages.
  • Trusting hypopg results without testing the real index: it only shows what the planner would choose, not the actual run time.

LimitsWhen not to use it

Do not install extensions "in case". Each one adds upgrade work, security surface and sometimes shared memory and hooks on every query. On managed services, use only the provider's supported list and versions, and check major-version compatibility before every upgrade.

SecuritySecurity notes

Extensions run code inside the server with high privileges. Install only trusted, maintained packages from your distribution or vendor, pin versions, restrict CREATE on the database so only administrators can install extensions, and review untrusted extensions (and their SECURITY DEFINER functions) before allowing them. pg_cron jobs run as the role that scheduled them: keep that role's privileges minimal.

ChallengeTry it yourself

Write a query that lists every installed extension whose installed version is older than the version available on the server, which is the check to run after upgrading extension packages.

Show one solution
One solution
SELECT name, installed_version, default_version
FROM pg_available_extensions
WHERE installed_version IS NOT NULL
  AND installed_version <> default_version
ORDER BY name;

InterviewQuestions you should be able to answer

Which extensions do you install on every production PostgreSQL server, and why?
pg_stat_statements for per-query cost, auto_explain with a duration threshold for plans of slow queries, and pgaudit where compliance requires an audit trail. Commonly also pg_cron for in-database scheduling, pg_partman for time partitions, pg_prewarm with autoprewarm for faster recovery after restarts, and pgstattuple plus pg_repack for bloat. Each preloaded one is added in a planned restart with packages present on all nodes.
A table is 70% bloated and cannot be locked for long. What do you do?
Confirm with pgstattuple or pgstattuple_approx, and check why autovacuum fell behind (long transactions, replication slots, thresholds). Then rebuild online with pg_repack or pg_squeeze, which copy the table while capturing concurrent changes and take only a brief exclusive lock at the swap; the table needs a primary key or unique not-null index and enough free disk for a full copy. VACUUM FULL is not an option because it holds ACCESS EXCLUSIVE for the whole rewrite. Fix the autovacuum cause afterwards.
How do you check whether a new index would help without building it?
hypopg creates hypothetical indexes in session memory; plain EXPLAIN then shows whether the planner would use them and the estimated cost. It is fast and safe on production, but it only reflects estimates, so build the real index CONCURRENTLY and verify with EXPLAIN ANALYZE before relying on it.

QuizCheck your understanding

5 questions with instant feedback in the interactive lesson; a wrong answer goes into your review deck.

Go deeperOfficial documentation