16 Extensions worth knowing
We loaded two million events, shaped like the book’s, into a table with one extension installed. One alter table and a loop later, the same rows took 30 MB instead of 324 MB with their indexes, and a month of deploy durations read about 300 pages instead of about 3,050. On a different server, a “who’s within 50 km of Frankfurt” query went from reading every page of its table to reading 112.
Neither is core Postgres. Both are extensions, which is how Postgres gets what other databases sell as separate products: new types and index methods, loaded into the server you already run (You don’t need another database (yet)). They’re also code you didn’t write, running inside your database process. Here are the ones worth knowing as of October 2026, what they cost, and how to vet one.
An extension is a package the database tracks
An extension is files on the server’s disk (a control file, SQL scripts, often a compiled library) that create extension turns into objects in one database, tracked together so drop extension and pg_dump treat them as one. Three things follow, and each bites somebody: it installs only what the package manager put on disk (which is why a managed service hands you a list), it’s per database, and the SQL objects’ version can drift from the library’s.
Some must be loaded at server start
Extensions that need shared memory, background workers, or planner hooks must be in shared_preload_libraries, which takes a restart.
If any library in shared_preload_libraries is missing or won’t load (a typo in the list, a package removed or not yet built for the new major version), the server refuses to start, and the restart you scheduled becomes an outage. Install the package first, and check the setting on a staging server or replica before you restart the primary.
pg_stat_statements is the classic trap the other way round: it installs fine without the preload, then fails when you read it with must be loaded via "shared_preload_libraries".
Prove it · lab 15-extensions.sh
Trusted extensions don’t need a superuser
Since PostgreSQL 13, an extension marked trusted, one that “cannot provide access to outside-the-database functionality”, can be installed by any role with CREATE on the database: citext, pg_trgm, btree_gist, ltree, yes; postgres_fdw, which opens connections, no. On a managed service there’s no superuser at all, so check the provider’s list before you design around anything here (Extensions come from an allowlist).
Deep dive: Which extensions are trusted, and what a plain role can do
That’s pg_available_extension_versions for each one’s default version. A role with CREATE on the database installed citext and wrote PL/pgSQL; it got 42501 for postgres_fdw and for PL/Python.
Prove it · lab 15-extensions.sh
Versions move with alter extension update
After every package, minor, or major upgrade, run alter extension ... update for each extension, in each database. Nothing does it for you, managed services included (The provider adds its own gate on CREATE EXTENSION). And a major upgrade waits until every extension has a build for the new major.
Deep dive: How Postgres chains upgrade scripts
Extensions ship scripts named extension--from--to.sql, and Postgres chains them. From hstore 1.4, pg_extension_update_paths('hstore') gives the path to 1.8 as 1.4--1.5--1.6--1.7--1.8, and alter extension hstore update runs all four. The same command moved TimescaleDB from 2.29.2 to 2.30.2 in our lab, because its image keeps older library versions on disk for exactly that. pg_upgrade carries the SQL objects over and writes a script for any updates still pending.
Prove it · lab 15-extensions.sh
Contrib is the safest shelf, though some of it is obsolete
About fifty extensions ship in contrib, on Postgres’s own release cycle and license. They’re the safest there are, and a few are obsolete:
| Module | Use it for | Still needed? |
|---|---|---|
pg_stat_statements | per-query statistics | yes, everywhere |
pg_trgm | like '%x%', similarity | yes |
btree_gist | scalars in exclusion constraints | yes |
pgcrypto | password hashing, PGP encryption | for crypt() and PGP only |
citext | case-insensitive text | mostly replaced by lower() indexes |
hstore | flat key-value column | mostly replaced by jsonb |
uuid-ossp | UUID generation | only for v1, v3, v5 |
Core has gen_random_uuid(), uuidv7(), and sha256(). For case-insensitive text, our default is a unique index on lower(), and citext’s own docs point to nondeterministic collations as the other alternative (For emails, make lower() the unique key). If you use pgcrypto’s PGP functions, be on 18.6 or later: before it, a cipher OpenSSL refused (under FIPS, say) silently produced trivially breakable data (18.6 release notes).
PostGIS is why many teams run Postgres at all
PostGIS adds spatial types, functions, and GiST spatial indexing. It’s mature and widely offered: 3.6.4 (June 2026), for PostgreSQL 12 through 18, under the GPL v2 or later, which attaches to distributing it, not running it behind your app.
The type decides what distance means
PostGIS has two families of types, and the choice decides what “distance” means. geometry works on a flat plane, in the units of its spatial reference system (SRID): degrees for longitude and latitude (4326), meters for a projection. geography works on the curved Earth, in meters. Frankfurt to Amsterdam, four ways:
The real answer is about 364.5 km. geometry in 4326 says 4.4 “degrees”, meaningless. Web Mercator, the projection of most web maps, says 582 km, 60 percent too long. Use geography for points spread across the world, or geometry in a projected SRID for regional data. Never compute distances in degrees.
Deep dive: The four distances, as SQL
(Output rounded.) A degree of longitude shrinks as you go north, and Mercator stretches distances more the farther you get from the equator. Store everything in one SRID; mixing them is an error at best and a wrong answer after a careless st_transform at worst.
Prove it · lab 15-extensions.sh
ST_DWithin uses the index; ST_Distance doesn’t
Give each of the book’s 100,000 users a last-seen geography point around one of the five seed regions, with a GiST index on it. “Users within 50 km of Frankfurt”:
Read it from the bottom: a bounding-box test (&& against a box grown by 50 km) found 1,615 candidates in 48 index pages, then the exact distance kept 955.
Now the version everybody writes first, st_distance(last_seen, ...) < 50000: same 955 rows, and a sequential scan of every page, because nothing tells the planner that “under 50 km” implies “inside this box”. Intersecting a st_buffer circle uses the index but draws the circle with 32 sides, and missed 6 users. Use ST_DWithin. Always.
The index finds rows; physical order decides pages
Look at the buffers again: 773 pages for 955 rows, from a table of 834. Nearby users were inserted in id order, so they sit on 725 different pages. cluster user_locations using user_locations_gist rewrote the table in spatial order, and the same query read 112 pages. cluster locks the table for the rewrite and the order decays as rows change, so it suits tables loaded in bulk.
Nearest neighbors, the other query spatial apps live on, is order by last_seen <-> point limit 5, which walked the index in distance order and read 12 pages.
PostGIS’s own bill comes due at upgrade time: it links against the GEOS and PROJ libraries, and a major Postgres upgrade waits until PostGIS is built for the new major.
Deep dive: The box test, proven, plus the clustered and nearest-neighbor plans and what PostGIS costs
Prove it · lab 15-extensions.sh
The <-> index is used only in an order by, and for geography it orders on a sphere, not the spheroid, so a near-tie can come back slightly out of order; compute st_distance on the rows you return.
For the upgrade itself, postgis_extensions_upgrade() moves PostGIS and its companions together. It has many companion extensions; install only what you use. And a GiST index stores bounding boxes, so a sprawling polygon’s box matches far more than the polygon; the docs recommend indexing ST_Subdivide pieces.
Prove it · lab 15-extensions.sh
TimescaleDB is partitioning with opinions
TimescaleDB turns a table into a hypertable that creates child tables (chunks) by time on the fly, with a columnar format for older chunks and incrementally maintained rollups. We ran 2.30.2, the current release, on PostgreSQL 18; its company, formerly Timescale, is now Tiger Data.
A hypertable is automatic partitioning
Native partitioning (Partition for the delete) makes you create every partition before rows arrive. A hypertable makes chunks as needed: create table events (...) with (tsdb.hypertable, tsdb.partition_column = 'created_at', tsdb.chunk_interval = '7 days'), and inserting the book’s year of events created 53 weekly chunks with no further DDL.
The columnstore: 324 MB to 30 MB
The columnstore rewrites a chunk into batches of up to 1,000 rows, each column of a batch stored as one compressed array, grouped by a segmentby column and sorted by an orderby:
A month of deploy durations read about 3,050 pages from row chunks and about 300 from the columnstore, which skipped batches outside the month and decompressed only the columns the query names. Our time-ordered data is the shape the format is built for; yours may compress less. The cost moves to writes: updating compressed rows decompresses and recompresses batches, so it pays off for data that’s append-mostly once it’s a few days old.
Prove it · lab 15-extensions.sh
Deep dive: Inside the columnar plan, and the conversion loop
In production, call add_columnstore_policy('events', after => interval '7 days') does it and leaves the newest chunks as rows for fast inserts. The ratio comes from hypertable_columnstore_stats('events'). One chunk of the month query:
(The feature was called “compression” before 2.18; the old function names still work.)
Continuous aggregates lag until a refresh
A continuous aggregate is a materialized view over a hypertable that TimescaleDB refreshes incrementally, recomputing only the time buckets whose rows changed. The default since 2.13 surprises people: they’re materialized only. An event inserted for October 5 didn’t show up in our daily rollup until a refresh covered that day. Set timescaledb.materialized_only = false to union in fresh rows, or schedule refreshes with a policy.
Deep dive: The daily rollup that missed one deploy
It holds 1,826 rows, one per day and kind. After one more deploy on October 5, the view said 1,431 and the table 1,432, until the next refresh.
Prove it · lab 15-extensions.sh
Off Tiger Data’s cloud, expect hypertables at most
Read this before you build on it. The LICENSE splits the repository: code outside the tsl directory is Apache 2.0, code inside it is under the source-available Timescale License (TSL). Use it in your own product all you like; you can’t sell it as a hosted database. The columnstore, continuous aggregates, retention policies, and the job scheduler are TSL (editions), so on a managed service other than Tiger Data’s own, expect hypertables at most.
TimescaleDB earns its place where the columnstore changes the size of the machine you need. If your time series is one events table and retention is “drop old months”, native partitioning with pg_partman and BRIN is less machinery and no license question.
pg_cron and pg_partman keep time on your side
pg_cron (PostgreSQL license; 1.6.8) runs SQL on a cron schedule from a preloaded background worker, down to one-second intervals. Jobs fail over with the database, with no scheduler host to run. Every run lands in cron.job_run_details, which nothing cleans, so schedule a delete for it too.
pg_partman (PostgreSQL license; 5.5.0) manages native partitions: it creates them ahead of time and drops what falls out of retention each time run_maintenance() runs, from pg_cron or its own worker (Create partitions before you need them).
Rebuild bloated tables without locking them
vacuum makes dead space reusable but rarely gives it back; vacuum full gives it back by rewriting the table under an ACCESS EXCLUSIVE lock (VACUUM FULL is an outage on a big table). Two extensions do the same rewrite while you keep using the table.
We bloated three identical tables to 85 MB, about three times their live data, and rebuilt each one while using it. vacuum full shrank its copy to a third, and while it ran, even a plain select hit its lock_timeout. pg_repack (BSD; 1.5.3) shrank its copy the same amount while an insert went straight through. pg_squeeze (PostgreSQL-style license; 1.9.4) did the same.
Both write the whole table to WAL, which replicas and backups will feel. Fix whatever caused the bloat first.
Deep dive: How pg_repack and pg_squeeze catch the writes, and what they need
pg_repack logs every change through a trigger into a side table, copies the table, builds its indexes, replays the log, and swaps the files, holding ACCESS EXCLUSIVE only at the start and for the swap. It needs a primary key or a not-null unique index and room for a full copy. Before it copies, it waits for every older transaction: a forgotten idle-in-transaction session stalls it indefinitely.
pg_squeeze uses logical decoding instead, so writers pay no trigger cost. It needs shared_preload_libraries, wal_level = logical before PostgreSQL 19, and, since 18.6, its name in output_plugin_libraries, the new allowlist of decoding plugins; without it, squeeze_table fails with “may not be used as an output plugin”. PostgreSQL 19, in beta as of October 2026, brings REPACK CONCURRENTLY into core.
Prove it · lab 15-extensions.sh
pgaudit logs who did what, at the cost of log volume
pgaudit (PostgreSQL license; 18.0) logs statements by class (read, write, ddl, role, and more) in the fixed, parseable format auditors ask for, or narrows to particular tables through a role’s grants. It must be preloaded. The cost is log volume: audit writes and DDL broadly, reads narrowly (Start auditing with the server log; it’s free).
Ask the planner hypothetical questions
HypoPG (PostgreSQL license; 1.4.3) creates hypothetical indexes that plain explain sees and no disk holds. A lookup on users.email read all 1,126 pages; with a hypothetical index, explain picked it before we built anything. Test a column order or a partial index’s predicate in seconds. pg_hint_plan (BSD; 1.8.0) goes the other way and dictates plans through comments; keep it for incidents (pg_hint_plan is for the night a plan flips).
Deep dive: What HypoPG shows, and what it doesn’t
explain analyze ignores hypothetical indexes, so the real run still read 1,126 pages; after building the real index it read under 5. hypopg_relation_size() estimated the index within 25 percent, and hypopg_hide_index() hides a real index from your session to ask “what if I dropped it?”.
Prove it · lab 15-extensions.sh
PGroonga searches text the built-in parser can’t split
pgvector and the BM25 search extensions have their own chapter, with each one’s license and where it runs (Search extensions put BM25 in the index). One more belongs here: PGroonga (PostgreSQL license; 4.0.9) searches Japanese and Chinese text, which the built-in parser, finding words by spaces, can’t.
Apache AGE is a nicer language for the same joins
Apache AGE (Apache 2.0; 1.8.0) adds openCypher, the graph query language, over ordinary tables. A match like -[:PARENT_OF*1..]-> is the recursive CTE in a nicer language; each hop is still a join, so the performance argument stands. Every session must load 'age', which fights poolers, and the project went quiet from September 2024 to May 2025 (board reports).
Deep dive: The team tree in Cypher
It returns "Platform", "Databases", "Postgres", as agtype. The graph lives in one table per label, like org."Team" and org."PARENT_OF".
Run an analytics engine on a replica, never the primary
Row storage reads whole rows, which is why wide scans are where Postgres loses (Rollups and a column extension close much of the gap). pg_duckdb (MIT; 1.1.1) embeds DuckDB’s columnar engine and reads Parquet and Iceberg; pg_lake (Apache 2.0) puts lake files behind tables. Some fade: ParadeDB archived pg_analytics, and pg_mooncake’s last published release was in February 2025 (a v0.1.3 tag followed that December). Each is a second engine in your process; run it on a replica, never your OLTP primary.
pgmq is still a Postgres table
pgmq (PostgreSQL license; 1.13.0) wraps the SKIP LOCKED queue (A Postgres queue commits with the business write) in a cloud-queue API: send(), read() with a visibility timeout, archive(). It’s still a Postgres table, with the same vacuum needs.
Untrusted languages need a superuser
PL/pgSQL is trusted: it can’t do anything SQL can’t (Code in the database). Untrusted languages (plpython3u, plperlu) run as the server’s OS user, so only superusers write them, and managed services leave them out. Trusted JavaScript exists in PL/v8 and PLJS. Python in the database usually belongs in the application.
A crashing extension drops every connection, so vet it
An extension’s code runs inside the backend, and when a backend crashes, the server ends every other session: one bad function call, and every connection drops. Before you depend on one, ask:
- Is it maintained? Release dates, how fast it supported the last major version, how many people commit.
- What license, for which parts? Read the whole LICENSE: some projects split by directory.
- Will it be there for the next major version? Your upgrade waits for every extension’s build.
- Can you run it where you run Postgres? The provider’s list, and whether it needs preloading or a superuser.
- What does it do to your process? Workers, shared memory, hooks on every statement, WAL.
- What’s the exit? An extension that owns your data’s type (PostGIS, pgvector) is a long-term commitment.
Then test its upgrade path in staging.
Deep dive: Every extension in this chapter, at a glance
As of October 2026. “Preload” means shared_preload_libraries.
| Extension | What it adds | License | Source |
|---|---|---|---|
| PostGIS 3.6.4 | spatial types, GiST spatial search | GPL v2+ | third party |
| TimescaleDB 2.30.2 | hypertables, columnstore, rollups | Apache 2.0 + TSL | third party, preload |
| pgvector 0.8.7 | vectors, HNSW and IVFFlat | PostgreSQL | third party |
| Apache AGE 1.8.0 | openCypher graphs | Apache 2.0 | third party |
| pg_cron 1.6.8 | SQL on a schedule | PostgreSQL | third party, preload |
| pg_partman 5.5.0 | partition creation and retention | PostgreSQL | third party |
| pg_repack 1.5.3 | online rebuild, trigger-based | BSD 3-clause | third party |
| pg_squeeze 1.9.4 | online rebuild, logical decoding | PostgreSQL-style | third party, preload |
| pgaudit 18.0 | classed, parseable audit log | PostgreSQL | third party, preload |
| HypoPG 1.4.3 | hypothetical indexes | PostgreSQL | third party |
| pg_hint_plan 1.8.0 | planner hints in comments | BSD 3-clause | third party, load or preload |
| PGroonga 4.0.9 | full-text search for every language | PostgreSQL | third party |
| pg_duckdb 1.1.1 | DuckDB engine, lake files | MIT | third party |
| pgmq 1.13.0 | message-queue API on tables | PostgreSQL | third party |
| PL/v8 3.2.5, PLJS 1.0.5 | trusted JavaScript | PostgreSQL-style | third party |
Contrib (pg_stat_statements preloaded, auto_explain, pg_prewarm, hstore, ltree, citext) is PostgreSQL-licensed and ships with the server.
The short version
- An extension is files on the server plus SQL objects in one database. Some need
shared_preload_librariesand a restart; all needalter extension ... updateafter upgrades. - Drop
uuid-osspand most ofpgcryptoandcitext: core functions andlower()indexes cover them. - In PostGIS, use
geographyor a projected SRID, and filter withST_DWithin, which used the index, neverST_Distance, which read every page. - TimescaleDB’s columnstore took our events from 324 MB to 30 MB and a month’s query from about 3,050 pages to about 300. It’s Timescale License code: free to self-host, not to resell.
pg_repackandpg_squeezerebuild bloated tables online wherevacuum fullblocks even reads.- Test an index with HypoPG before you build it.
- Vet every extension for maintenance, license, next-major support, availability, and crash risk.
Further reading
- Packaging Related Objects into an Extension, PostgreSQL documentation. Control files, trusted extensions, update scripts, and how to write your own.
- Additional Supplied Modules and Extensions, PostgreSQL documentation. Every contrib module, with which ones are trusted.
- PostGIS manual: spatial queries, PostGIS project. Index use,
ST_DWithin, and nearest-neighbor search, with the reasons behind each. - TimescaleDB editions, Tiger Data. Which features are Apache 2.0 and which are under the Timescale License.
- pg_repack documentation, reorg project. Requirements, locks, and options for online rebuilds.
- HypoPG documentation, HypoPG project. Hypothetical indexes and hidden indexes, with their limits.