Mid-level 12 min · March 05, 2026

PostgreSQL Extensions — Replica Crash When .so Missing

After installing pg_stat_statements on primary, replica crashed with 'could not load library' error and infinite lag.

N
Naren Founder & Principal Engineer

20+ years shipping high-throughput database systems. Everything here is grounded in real deployments.

Follow
Production
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Extensions are bolt-on modules that inject new types, functions, or index methods into PostgreSQL
  • CREATE EXTENSION reads a .control file, executes an SQL script, and registers objects in pg_extension
  • shared_preload_libraries is required for some extensions — missing it causes silent failure
  • HNSW indexes in pgvector cost 2-3x more disk space than IVFFlat but give ~5% better recall
  • Production trap: upgrading the OS package without running ALTER EXTENSION UPDATE leaves the DB version stale
✦ Definition~90s read
What is PostgreSQL Extensions?

PostgreSQL extensions are shared libraries (.so files on Linux, .dylib on macOS) that hook into the database's internal function call mechanism via the CREATE EXTENSION command. When you run CREATE EXTENSION pgvector, PostgreSQL dynamically loads pgvector.so into its process memory, registering new data types, operators, index access methods, and functions.

Think of PostgreSQL like a smartphone.

The extension's SQL schema (typically in a .sql control file) defines the SQL-level objects, but the actual implementation lives in the compiled C library. This is why every replica must have the exact same .so file at the same filesystem path — if a replica tries to apply WAL records that reference a function from a missing extension, the backend process crashes with a could not access file error because the dynamic linker can't resolve the symbol at runtime.

This isn't a configuration mistake; it's a fundamental constraint of how PostgreSQL's extension model works: shared libraries are loaded per-backend, not per-cluster, and WAL replay doesn't install software.

Internally, PostgreSQL's extension loading follows a strict lifecycle. The pg_available_extensions view lists what's installed on disk in SHAREDIR/extension/, while pg_extension tracks what's actually created in a database. When a backend starts a transaction that references an extension function, the dynamic loader (dlopen()) maps the .so into the backend's address space.

The extension's _PG_init() function runs, registering callbacks for everything from custom scan methods to background workers. If the .so is missing, dlopen() returns NULL, and PostgreSQL's error handler can't gracefully degrade — it terminates the backend because the function pointer table has a null entry.

This is why ALTER EXTENSION UPDATE can silently break replication if the new .so isn't deployed to replicas first.

In production, you manage this by treating extensions as infrastructure, not database objects. Use the same package manager (apt, yum, or custom RPMs) across all nodes, pinning exact versions. For self-compiled extensions, include the .so in your deployment artifact and validate with pg_config --sharedir and ldd on the binary.

Never rely on CREATE EXTENSION IF NOT EXISTS in migration scripts without ensuring the library is present — that only checks the SQL catalog, not the filesystem. Tools like pgxn or pgrx help, but they don't solve the distribution problem. The canonical failure pattern: a developer adds pgvector to a schema migration, deploys to the primary, the WAL ships to a replica that lacks vector.so, and the replica crashes with FATAL: could not load library.

The fix is always the same: install the .so on all nodes before running CREATE EXTENSION on any node.

Plain-English First

Think of PostgreSQL like a smartphone. It comes with a camera, phone app, and messages out of the box — but you can install apps to do way more. PostgreSQL extensions are those apps: bolt-on features that live inside the database itself, like GPS navigation or a video editor. You choose exactly which 'apps' your database needs. No bloat, no rewrites — just enable what you want and it's ready to use.

PostgreSQL ships as one of the most capable relational databases on the planet, but its real competitive edge isn't what it does by default — it's what it can become. Extensions let PostgreSQL morph into a time-series engine, a geospatial powerhouse, a vector similarity search system, or a statistical analysis platform without ever leaving SQL. This isn't a niche feature — it's the architectural choice that lets a single Postgres cluster replace entire categories of specialised databases.

The problem extensions solve is elegant: database internals are hard to change safely at runtime, but application requirements change constantly. Before extensions, adding new data types, indexing strategies, or procedural languages meant patching the core and recompiling. Extensions formalise a safe, versioned, reversible mechanism for injecting new capabilities into a live cluster — complete with dependency tracking, upgrade paths, and schema isolation. They're the reason PostGIS can add full geographic primitives and pgvector can power AI embedding search inside the same Postgres instance your users' accounts live in.

By the end of this article you'll know exactly how extensions work under the hood — from the shared object loading mechanism to the extension control file format. You'll be able to install and audit extensions safely in production, understand the performance implications of popular extensions, and build a minimal custom extension from scratch. No fluff, just the internals and the gotchas that bite in production.

Why PostgreSQL Extensions Are Shared Libraries That Must Be Present on Every Replica

PostgreSQL extensions are dynamically loaded shared libraries (.so files) that hook into the database's internal APIs to add new data types, index methods, or procedural languages. Unlike application-level plugins, these libraries execute inside the database process itself — meaning a missing .so on a replica doesn't just break a query; it crashes the entire backend. The core mechanic is simple: CREATE EXTENSION records the library name in the system catalog, and at runtime, PostgreSQL uses dlopen() to load it. If the file isn't on disk at the expected path, the process segfaults.

In practice, extensions like PostGIS or pg_cron ship as .so files that must be installed identically on every node in a replication cluster. The extension's SQL objects (functions, operators, types) are replicated via WAL, but the underlying shared library is not. This asymmetry is the root cause of replica crashes: the SQL references a function that requires the library, but the library isn't there. The failure is immediate and unrecoverable — the replica process dies, and replication stalls until manual intervention.

Use extensions when you need deep integration with PostgreSQL internals — custom index access methods, foreign data wrappers, or specialized data types. Avoid them for simple application logic that could live in a schema or a separate service. In production, every extension you add becomes a deployment dependency: the .so must be present on all nodes before the CREATE EXTENSION runs. This is non-negotiable.

Replica Crash Is Not a Configuration Error
A missing .so on a replica doesn't produce a graceful error — it kills the backend instantly. No retry, no fallback, just a segfault and a broken replication stream.
Production Insight
Teams deploying PostGIS on a primary but forgetting to install the .so on read replicas cause cascading replica crashes the moment any query touches a geography column.
The symptom is a sudden spike in 'terminating connection due to administrator command' logs, followed by replication lag that never recovers.
Rule: always install extension .so files on every node before running CREATE EXTENSION — even on replicas that will never serve queries.
Key Takeaway
Extensions are shared libraries loaded into the database process — missing .so files crash backends, not just break queries.
The .so is never replicated via WAL; you must install it manually on every node before creating the extension.
Prefer built-in features or schema-level solutions over extensions unless you need deep internal hooks — each extension adds a deployment and upgrade dependency.
PostgreSQL Extension .so Missing Causes Replica Crash THECODEFORGE.IO PostgreSQL Extension .so Missing Causes Replica Crash Flow from extension install to replica failure when shared library absent Extension as Shared Library (.so) Loaded into PostgreSQL process memory CREATE EXTENSION Installs .so + SQL Copies files to shared/extension directories Replica Starts Without .so Missing library on standby node Replica Crash on Extension Call PostgreSQL process aborts due to missing symbol Fix: Sync .so to All Replicas Ensure identical extension files across cluster ⚠ Missing .so on replica causes crash on first extension use Always deploy extension .so files to all nodes before CREATE EXTENSION THECODEFORGE.IO
thecodeforge.io
PostgreSQL Extension .so Missing Causes Replica Crash
Postgresql Extensions

How PostgreSQL Extensions Actually Work Internally

Every extension is made of three things: a shared object file (.so on Linux, .dll on Windows), a SQL script that defines objects in the database, and a control file that ties it all together. When you run CREATE EXTENSION, Postgres reads the control file from $sharedir/extension/, executes the install SQL script, and registers every object the extension created in the pg_extension and pg_depend system catalogs. That catalog registration is the secret sauce — it means Postgres knows which tables, functions, operators, and types belong to the extension, so DROP EXTENSION CASCADE can clean up everything safely.

The shared object is loaded into the backend process on first use via dlopen(). This means extension code runs in the same memory space as PostgreSQL itself. A poorly written C extension can segfault the entire backend — there's no sandbox. That's why extensions from trusted sources and your Linux package manager (postgresql-16-postgis-3, for example) are fundamentally safer than compiling random GitHub repos in production.

Extensions live in a specific schema (default: public, but you can redirect with the schema parameter). The search_path matters enormously here — if the extension's schema isn't on your search_path, function calls will fail with 'function not found' even though the extension is installed. Always check \dx in psql and pg_extension in SQL to see exactly what's active and which schema it landed in.

inspect_extensions.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
-- ============================================================
-- Inspect everything PostgreSQL knows about installed extensions
-- ============================================================

-- 1. List all currently installed extensions with version and schema
SELECT
    extname          AS extension_name,
    extversion       AS installed_version,
    nspname          AS target_schema,
    extrelocatable   AS can_be_moved_to_other_schema
FROM pg_extension
JOIN pg_namespace ON pg_namespace.oid = pg_extension.extnamespace
ORDER BY extname;

-- 2. Show every object owned by the pg_stat_statements extension
-- This reveals what CREATE EXTENSION actually created
SELECT
    classid::regclass   AS catalog_table,   -- which system catalog the object lives in
    objid::regprocedure AS object_signature -- the object itself, cast to readable form
FROM pg_depend
WHERE deptype = 'e'   -- 'e' means "owned by an extension"
  AND refobjid = (SELECT oid FROM pg_extension WHERE extname = 'pg_stat_statements')
LIMIT 20;

-- 3. Check which extensions are AVAILABLE to install (not yet installed)
SELECT
    name             AS extension_name,
    default_version  AS latest_version,
    comment          AS description
FROM pg_available_extensions
WHERE installed_version IS NULL   -- NULL means not installed yet
ORDER BY name;
Output
extension_name | installed_version | target_schema | can_be_moved_to_other_schema
-----------------+-------------------+---------------+------------------------------
pg_stat_statements | 1.10 | public | f
plpgsql | 1.0 | pg_catalog | f
uuid-ossp | 1.1 | public | t
(3 rows)
catalog_table | object_signature
----------------+------------------------------------------
pg_proc | pg_stat_statements(integer,boolean)
pg_proc | pg_stat_statements_reset(oid,oid,bigint)
pg_proc | pg_stat_statements_info()
pg_class | pg_stat_statements
(4 rows)
Architecture Insight:
extrelocatable = false means the extension hard-codes its schema at install time — you cannot ALTER EXTENSION ... SET SCHEMA later. pg_stat_statements and plpgsql are both non-relocatable. Plan your schema layout before installing in production, not after.
Production Insight
The shared object runs in the backend's memory space — a segfault in an extension can crash the entire PostgreSQL backend process.
Trusted OS-package extensions are safer than compiling random GitHub repos.
Rule: when debugging crashes, isolate whether the extension .so is the cause by temporarily removing shared_preload_libraries entries.
Key Takeaway
pg_depend tracks every object an extension creates.
That's why DROP EXTENSION CASCADE is safe — Postgres knows exactly what to remove.
Always inspect pg_extension before making schema changes.

Installing and Managing Extensions in Production — The Right Way

Installing an extension is one line of SQL, but doing it safely in production involves four distinct steps that most tutorials skip entirely.

First, the shared library must exist on the filesystem of every PostgreSQL server in your cluster — including replicas, because replay of CREATE EXTENSION on a standby will fail if the .so file isn't present. This means your deployment pipeline must install the OS package before the SQL runs, not after.

Second, some extensions require preloading into shared memory at startup via shared_preload_libraries in postgresql.conf. pg_stat_statements is the classic example — if it's not in that list, CREATE EXTENSION succeeds but all the views return zero rows and no error is raised. Silent failure at its most frustrating.

Third, only superusers can CREATE EXTENSION by default. In managed cloud environments (RDS, Cloud SQL, AlloyDB) you get a pseudo-superuser role like rds_superuser that can install from a pre-approved allowlist. You cannot install arbitrary extensions on managed Postgres — this is a deliberate security boundary.

Fourth, extension upgrades are separate from OS package upgrades. Updating the debian package gets you new .so and SQL files on disk, but the database still runs the old version until you explicitly run ALTER EXTENSION name UPDATE. Both steps are required and order matters: package first, ALTER EXTENSION second.

production_extension_lifecycle.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
-- ============================================================
-- Full production lifecycle: install, verify, upgrade, remove
-- ============================================================

-- STEP 1: Add to postgresql.conf BEFORE attempting install
-- (Do this via ALTER SYSTEM so it's tracked in postgresql.auto.conf)
ALTER SYSTEM SET shared_preload_libraries = 'pg_stat_statements, pg_prewarm';
-- This requires a cluster restart — check pending restart status:
SELECT name, setting, pending_restart
FROM pg_settings
WHERE name = 'shared_preload_libraries';

-- STEP 2: After restart, install the extension into a dedicated schema
-- Using a dedicated schema prevents extension objects polluting 'public'
CREATE SCHEMA IF NOT EXISTS monitoring;
CREATE EXTENSION IF NOT EXISTS pg_stat_statements
    SCHEMA monitoring;   -- only works if extension is relocatable

-- For non-relocatable extensions (like pg_stat_statements), install into public
-- and control access via GRANT instead:
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
REVOKE ALL ON FUNCTION pg_stat_statements_reset() FROM PUBLIC;
GRANT  EXECUTE ON FUNCTION pg_stat_statements_reset() TO monitoring_role;

-- STEP 3: Verify it's collecting data (proves shared_preload_libraries worked)
SELECT
    calls,
    mean_exec_time::numeric(10,3) AS avg_ms,
    LEFT(query, 80)               AS query_preview
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 5;

-- STEP 4: Upgrade workflow (after OS package update)
-- Check current vs available version
SELECT
    name,
    installed_version,
    default_version,
    (installed_version <> default_version) AS upgrade_available
FROM pg_available_extensions
WHERE name = 'pg_stat_statements';

-- Run the upgrade (executes the delta SQL script, e.g. pg_stat_statements--1.9--1.10.sql)
ALTER EXTENSION pg_stat_statements UPDATE;         -- updates to latest
ALTER EXTENSION pg_stat_statements UPDATE TO '1.10'; -- pin to specific version

-- STEP 5: Safe removal — CASCADE drops all dependent objects too
-- Without CASCADE, DROP fails if any user object depends on extension types
DROP EXTENSION IF EXISTS pg_stat_statements CASCADE;

-- Confirm it's gone
SELECT COUNT(*) AS remaining_installations
FROM pg_extension
WHERE extname = 'pg_stat_statements';
Output
name | setting | pending_restart
---------------------+--------------------------------------+----------------
shared_preload_libraries | pg_stat_statements, pg_prewarm | t
(1 row)
-- (restart required)
calls | avg_ms | query_preview
-------+---------+------------------------------------------------------------------
8421 | 142.337 | SELECT * FROM orders WHERE customer_id = $1 AND status = $2
3102 | 23.811 | INSERT INTO events (type, payload, created_at) VALUES ($1, $2, $3)
891 | 891.204 | SELECT COUNT(*) FROM order_items JOIN products ON ...
(3 rows)
name | installed_version | default_version | upgrade_available
--------------------+-------------------+-----------------+-------------------
pg_stat_statements | 1.9 | 1.10 | t
(1 row)
remaining_installations
------------------------
0
(1 row)
Watch Out:
On streaming replication setups, the OS package containing the extension's .so file must be installed on every replica before you run CREATE EXTENSION on the primary. WAL replay on the standby will attempt to load the .so and crash the replica process with 'could not load library' if it's missing — causing replication lag or a full replica reset.
Production Insight
The separation between OS package upgrade and ALTER EXTENSION UPDATE is a common source of production outages.
Never assume a package update upgrades the extension in the database.
Rule: automate both steps in tandem — deploy package, then run ALTER EXTENSION UPDATE as a SQL migration.
Key Takeaway
shared_preload_libraries is a silent failure trap.
If an extension requires it and it's missing, CREATE EXTENSION works but the extension does nothing.
Always verify with a functional test — not just a successful DDL.

Three Extensions You Should Know Deeply — pgvector, PostGIS, and pg_partman

Knowing how to run CREATE EXTENSION is table stakes. Knowing the performance model and operational nuances of specific extensions is what separates a database engineer from someone who just read the docs.

pgvector adds vector data types and approximate nearest-neighbour search, making Postgres a viable store for AI embedding search. Its HNSW index (added in 0.5.0) dramatically outperforms the older IVFFlat index for most workloads, but HNSW builds are memory-intensive — each connection building the index uses maintenance_work_mem, and building in parallel multiplies that.

PostGIS is the gold standard for geospatial work, but it adds two extension layers: postgis (core) and optionally postgis_topology and postgis_raster. The ST_DWithin function with a geography (not geometry) column correctly handles great-circle distance but is ~10x slower than geometry unless you have a spatial index on the column.

pg_partman automates partition maintenance — creating future partitions and dropping old ones on a schedule. It runs as background worker processes and requires pg_cron or a similar scheduler. The critical gotcha: pg_partman won't automatically attach data inserted into the parent table to the correct child partition unless you also configure partition_data correctly. Orphaned rows in the parent table silently kill query performance.

pgvector_hnsw_production.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
-- ============================================================
-- pgvector: store OpenAI embeddings and do similarity search
-- with an HNSW index tuned for production recall vs speed
-- ============================================================

CREATE EXTENSION IF NOT EXISTS vector;  -- installs the vector type and operators

-- Table storing article embeddings from a text-embedding-3-small model
-- (1536 dimensions for OpenAI's small model)
CREATE TABLE article_embeddings (
    article_id      BIGINT PRIMARY KEY,
    title           TEXT        NOT NULL,
    content_snippet TEXT        NOT NULL,
    embedding       vector(1536) NOT NULL,  -- the actual float array
    embedded_at     TIMESTAMPTZ  NOT NULL DEFAULT NOW()
);

-- Build an HNSW index.
-- m = max connections per node (higher = better recall, more RAM)
-- ef_construction = build-time search width (higher = better recall, slower build)
-- These values are a production starting point; tune with recall benchmarks.
CREATE INDEX article_embeddings_hnsw_idx
    ON article_embeddings
    USING hnsw (embedding vector_cosine_ops)  -- cosine similarity for NLP embeddings
    WITH (
        m               = 16,    -- default; go up to 64 for high-recall use cases
        ef_construction = 128    -- default 64; doubling improves recall ~2-5%
    );

-- At query time, set ef_search to control recall vs speed tradeoff
-- This is a per-session setting — set it in your app's connection pool setup
SET hnsw.ef_search = 100;  -- default 40; higher = better recall, slower query

-- Find the 5 most semantically similar articles to a given embedding
-- '[0.12, -0.34, ...]' would be your query vector from the embedding model
WITH query_vector AS (
    -- In practice this comes from your application layer as a parameter
    SELECT '[0.021, 0.043, -0.117, 0.089]'::vector(4) AS vec  -- toy 4-dim example
)
SELECT
    ae.article_id,
    ae.title,
    -- <=> is the cosine distance operator; 1 - distance = cosine similarity
    1 - (ae.embedding <=> qv.vec)   AS cosine_similarity,
    ae.content_snippet
FROM article_embeddings ae
CROSS JOIN query_vector qv
ORDER BY ae.embedding <=> qv.vec   -- ORDER BY distance ASC = most similar first
LIMIT 5;

-- Monitor index build progress (for large tables, HNSW build can take minutes)
SELECT
    phase,
    blocks_done,
    blocks_total,
    ROUND(100.0 * blocks_done / NULLIF(blocks_total, 0), 1) AS pct_complete
FROM pg_stat_progress_create_index
WHERE relid = 'article_embeddings'::regclass;
Output
article_id | title | cosine_similarity | content_snippet
------------+------------------------------------+-------------------+----------------------------------
882 | Introduction to Neural Networks | 0.97341 | A neural network is a system...
1204 | Deep Learning Fundamentals | 0.96108 | Deep learning refers to...
331 | Backpropagation Explained | 0.94872 | The backpropagation algorithm...
778 | Gradient Descent in Practice | 0.93210 | To minimise loss functions...
2041 | Activation Functions Compared | 0.91034 | ReLU, sigmoid, and tanh are...
(5 rows)
phase | blocks_done | blocks_total | pct_complete
--------------------+-------------+--------------+--------------
building index | 12400 | 50000 | 24.8
(1 row)
Pro Tip:
For pgvector HNSW indexes, set max_parallel_maintenance_workers = 4 and a generous maintenance_work_mem (e.g., 2GB) before building the index on large datasets. HNSW index builds are single-threaded per index, but the memory budget directly controls build quality. After the index is built, reset maintenance_work_mem to avoid memory pressure during normal operations.
Production Insight
pgvector's HNSW index build can exhaust maintenance_work_mem on large tables — monitor pg_stat_progress_create_index.
If the build fails with 'out of memory', reduce ef_construction or increase maintenance_work_mem.
Rule: benchmark recall vs speed with your own data — don't trust docs default values blindly.
Key Takeaway
HNSW gives near-exact recall but costs memory during build and larger disk footprint.
IVFFlat is faster to build and cheaper — right for frequent re-indexing.
Choose based on your query latency budget, not just recall numbers.

Building a Custom PostgreSQL Extension from Scratch

Building your own extension demystifies every extension you'll ever use and opens the door to organisation-specific functionality you can version, test, and deploy just like application code.

A minimal extension needs exactly three files: a control file (name.control), a SQL installation script (name--version.sql), and optionally a C file compiled to a shared object for performance-critical or type-level functionality. Pure SQL extensions are fully portable and need no compilation — they're underused and underappreciated.

The control file specifies the extension's identity: its default version, whether it's relocatable, what schema it prefers, and which other extensions it depends on. The requires field is how Postgres enforces extension dependency ordering — if your extension depends on uuid-ossp, Postgres will refuse to install yours without it.

The SQL script runs with the privileges of the user calling CREATE EXTENSION, inside a transaction. If any statement fails, the whole installation rolls back — a beautiful guarantee. Objects created in the script are automatically tagged as extension-owned in pg_depend, so you don't need to manage that yourself.

For production custom extensions, store them in a git repo, use PGXS (the extension build system that ships with PostgreSQL) to compile and install, and write pgTAP tests against your install/upgrade/uninstall scripts before they touch any real cluster.

build_custom_extension.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#!/usr/bin/env bash
# ============================================================
# Build a pure-SQL custom extension called 'safemath'
# that provides overflow-safe integer arithmetic helpers.
# No C compilation needed — pure SQL extensions are portable.
# ============================================================

set -euo pipefail

EXT_NAME="safemath"
EXT_VERSION="1.0"
# Find where PostgreSQL expects extension files
PG_SHAREDIR=$(pg_config --sharedir)
PG_EXTDIR="${PG_SHAREDIR}/extension"

echo "Installing ${EXT_NAME} to ${PG_EXTDIR}"

# ---- FILE 1: Control file ----
# This is what PostgreSQL reads when you call CREATE EXTENSION safemath
cat > "/tmp/${EXT_NAME}.control" << 'EOF'
# safemath extension
default_version = '1.0'
relocatable     = true
schema          = safemath
comment         = 'Overflow-safe arithmetic functions for financial calculations'
EOF

# ---- FILE 2: SQL installation script ----
# Named exactly: extname--version.sql  (double dash is required)
cat > "/tmp/${EXT_NAME}--${EXT_VERSION}.sql" << 'EOF'
-- SQL script runs inside a transaction during CREATE EXTENSION
-- The @extschema@ placeholder is replaced by the schema Postgres chose

-- Safe addition: returns NULL instead of overflowing
CREATE OR REPLACE FUNCTION @extschema@.safe_add(
    addend_a BIGINT,
    addend_b BIGINT
)
RETURNS BIGINT
LANGUAGE plpgsql
IMMUTABLE STRICT PARALLEL SAFE  -- PARALLEL SAFE lets planner use this in parallel queries
AS $$
BEGIN
    -- pg_catalog.int8 max is 9223372036854775807
    -- Check for overflow before it happens to avoid exception handling overhead
    IF addend_b > 0 AND addend_a > (9223372036854775807 - addend_b) THEN
        RETURN NULL;  -- would overflow positive
    END IF;
    IF addend_b < 0 AND addend_a < (-9223372036854775808 - addend_b) THEN
        RETURN NULL;  -- would overflow negative
    END IF;
    RETURN addend_a + addend_b;
END;
$$;

-- Safe multiply with the same NULL-on-overflow contract
CREATE OR REPLACE FUNCTION @extschema@.safe_multiply(
    factor_a BIGINT,
    factor_b BIGINT
)
RETURNS BIGINT
LANGUAGE sql
IMMUTABLE STRICT PARALLEL SAFE
AS $$
    SELECT
        CASE
            WHEN factor_b = 0 THEN 0
            WHEN ABS(factor_a) > (9223372036854775807 / ABS(factor_b)) THEN NULL
            ELSE factor_a * factor_b
        END;
$$;

-- Useful view: show current extension version at runtime
CREATE VIEW @extschema@.extension_info AS
    SELECT
        extname    AS name,
        extversion AS version
    FROM pg_extension
    WHERE extname = 'safemath';
EOF

# Copy files to where PostgreSQL can find them
sudo cp "/tmp/${EXT_NAME}.control"              "${PG_EXTDIR}/"
sudo cp "/tmp/${EXT_NAME}--${EXT_VERSION}.sql" "${PG_EXTDIR}/"

echo "Files installed. Now run in psql:"
echo "  CREATE EXTENSION safemath;"
echo "  SELECT safemath.safe_add(9223372036854775800, 100);  -- should return NULL"
Output
Installing safemath to /usr/share/postgresql/16/extension
Files installed. Now run in psql:
CREATE EXTENSION safemath;
SELECT safemath.safe_add(9223372036854775800, 100); -- should return NULL
Interview Gold:
The @extschema@ placeholder is substituted at install time by PostgreSQL's extension loader — it's not a SQL variable. This is why relocatable extensions work: the schema name is baked into function definitions at CREATE EXTENSION time, not hardcoded in the script. Interviewers love this question because most devs have never looked at an extension SQL file.
Production Insight
A failed custom extension install due to a syntax error can leave partial objects — always test in a disposable database.
Use pgTAP to test install/upgrade/uninstall scripts before deployment.
Rule: version your extension control file and SQL scripts in git; tag releases with semantic versions.
Key Takeaway
The @extschema@ placeholder is substituted at install time.
That's how relocatable extensions work — schema name is baked in at CREATE EXTENSION time.
Never hardcode schema names in extension SQL scripts.

Extension Security and Permission Models — The Defensive Side

Extensions run inside the database backend — no sandbox, no isolation. That means a malicious or buggy extension can read any data the backend can access, including memory of other connections. PostgreSQL's trust model relies on the superuser installing extensions, but after installation, the objects are owned by the superuser by default. However, any user can call extension functions if they have EXECUTE privilege, unless you revoke it.

For extensions like pg_stat_statements that expose performance data, the reset function is dangerous — any user can wipe hours of monitoring data if you don't revoke PUBLIC access. For extensions that create new data types (like pgvector or PostGIS), columns of those types can be accessed by any user if the table's permissions allow.

The safest pattern: install non-relocatable extensions in a dedicated schema, revoke all privileges from PUBLIC on extension functions, and grant only to specific roles. For relocatable extensions, use the SCHEMA option to isolate them. Always audit with \dp+ and check function permissions.

Also consider that extensions can introduce background workers (like pg_partman). These workers run with the privileges of the user who configured them — if that user is a superuser, the worker has full access. Limit background worker privileges to only what's needed.

extension_security_audit.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
-- ============================================================
-- Audit extension privileges and lock down dangerous functions
-- ============================================================

-- 1. List all extension functions with their privileges
SELECT
    n.nspname AS schema,
    p.proname AS function_name,
    pg_catalog.pg_get_function_identity_arguments(p.oid) AS args,
    pg_catalog.array_agg(
        CASE WHEN aclitem[1] IS NULL THEN 'PUBLIC'
             ELSE aclitem[1]::text
        END
    ) AS granted_to
FROM pg_proc p
JOIN pg_namespace n ON n.oid = p.pronamespace
LEFT JOIN pg_depend d ON d.objid = p.oid AND d.deptype = 'e'
WHERE d.refclassid = 'pg_extension'::regclass
GROUP BY n.nspname, p.proname, p.oid;

-- 2. Revoke EXECUTE on pg_stat_statements_reset() from PUBLIC
REVOKE ALL ON FUNCTION pg_stat_statements_reset() FROM PUBLIC;

-- 3. Grant only to monitoring_role (create the role first if needed)
CREATE ROLE monitoring_role;
GRANT EXECUTE ON FUNCTION pg_stat_statements_reset() TO monitoring_role;

-- 4. Check for extensions that have background workers consuming max_worker_processes
SELECT
    extname,
    extversion,
    pg_catalog.pg_stat_activity.pid,
    pg_catalog.pg_stat_activity.state
FROM pg_extension
LEFT JOIN pg_stat_activity ON pg_stat_activity.query LIKE '%' || extname || '%'
WHERE extname IN ('pg_partman', 'pg_cron', 'pg_prewarm');

-- 5. Verify that no extension-owned table has unwanted grants
SELECT
    schemaname,
    tablename,
    array_agg(privilege_type) AS privileges_granted
FROM pg_tables
JOIN pg_depend ON pg_depend.objid = (pg_tables.schemaname || '.' || pg_tables.tablename)::regclass
WHERE deptype = 'e'
  AND has_table_privilege('public', pg_tables.schemaname || '.' || pg_tables.tablename, 'SELECT')
GROUP BY schemaname, tablename;
Output
schema | function_name | args | granted_to
--------+-------------------------+-----------------------------+------------
public | pg_stat_statements_reset | oid,oid,bigint | {PUBLIC}
public | pg_stat_statements_info | | {PUBLIC}
(2 rows)
-- After REVOKE:
schema | function_name | granted_to
--------+-------------------------+--------------
public | pg_stat_statements_reset | {monitoring_role}
(1 row)
Mental Model: Extensions as Library Extensions
  • Any extension function you grant to PUBLIC can be called by any database user — no sandboxing.
  • Background workers (pg_partman, pg_cron) run as the role that configured them — if that's a superuser, they have full access.
  • Revoke EXECUTE on dangerous functions like reset() immediately after CREATE EXTENSION.
  • Install extensions in dedicated schemas to namespace their objects and control search_path access.
  • Audit extension object permissions regularly with queries against pg_depend and pg_proc.
Production Insight
A background worker from an extension running as superuser can be exploited if the extension has a vulnerability.
Limit background worker privileges by creating a dedicated role that owns only the necessary schema.
Rule: never let extension background workers run as superuser — create a restricted role for them.
Key Takeaway
Extensions run in the same memory space as PostgreSQL — no sandbox.
Revoke dangerous function access from PUBLIC immediately after install.
Audit extension permissions with pg_depend and pg_proc.

What Extensions Actually Are (And Why Most Devs Get Them Wrong)

Extensions aren't plugins. They're shared objects — compiled C libraries that get linked into the backend process at runtime. When you run CREATE EXTENSION, you're not installing software. You're registering a set of SQL objects (functions, operators, data types, casts) that call into that loaded library.

That distinction matters because it explains every painful production failure you'll ever see with extensions. The extension exists in two places: the filesystem (the .so file) and the database catalog (the registered objects). If those get out of sync — say you pg_dump from a system with PostGIS 3.0 and restore to one with 3.4 — your restore silently succeeds until someone queries a geometry column and the backend hard-crashes.

The catalog entry stores a version string, not a binary hash. PostgreSQL trusts that you've deployed the matching library. It does not verify. This is why extension upgrades must be explicit, never implicit during restores.

ExtensionCatalogCheck.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// io.thecodeforge — database tutorial

-- See exactly what's registered vs what's on disk
SELECT e.extname,
       e.extversion AS catalog_version,
       (SELECT setting FROM pg_settings WHERE name = 'server_version') AS pg_version,
       e.extrelocatable,
       e.extconfig IS NOT NULL AS has_config_table
FROM pg_extension e;

-- Find the actual .so file path for an extension
SELECT ae.name,
       ae.default_version,
       ae.installed_version,
       ae.comment
FROM pg_available_extensions ae
WHERE ae.name = 'postgis';
Output
extname | catalog_version | pg_version | extrelocatable | has_config_table
-----------+-----------------+------------+----------------+-----------------
postgis | 3.3.2 | 16.1 | f | t
(1 row)
Restore Trap:
pg_dump includes CREATE EXTENSION IF NOT EXISTS with the version from the source. If target has a different minor version, the restore won't fail — but queries on extension-owned types will segfault. Always run ALTER EXTENSION UPDATE after restore and test with a type-exercise query.
Key Takeaway
An extension is a contract between a .so file and the catalog. Break that contract and your database goes down hard.

When You Should (And Shouldn't) Use an Extension

Extensions solve real problems: adding vector search, geospatial types, partitioning orchestration, or full-text parsing improvements. But every extension you add is a deployment dependency and a potential crash vector. The C library runs in the same process as your backend. A segfault in pgvector takes down every connection on that server. No isolation.

Use extensions when: the feature is fundamental to your data model (PostGIS for location data), it saves you from writing and maintaining thousands of lines of procedural code (pg_partman), or it gives you a capability that would require a separate service (pgvector for embeddings).

Don't use extensions for: minor convenience, one-off analytics queries you can write in SQL, or anything with a CVE history you haven't vetted. Every extension is a supply chain risk. You're loading unsigned code into your database process. Check pgxn.org and the extension's GitHub issues before deploying. If the repo has 3 stars and the author hasn't pushed in 2 years, walk away.

And never install an extension "just to see what it does" on production. Development or a dedicated test instance only. I've watched a junior run CREATE EXTENSION on a prod replica and trigger a failover because the extension tried to allocate shared memory that didn't exist on the standby.

ExtensionRiskAudit.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// io.thecodeforge — database tutorial

-- Audit all installed extensions for trustworthiness
SELECT e.extname,
       e.extversion,
       (SELECT count(*)
        FROM pg_depend d
        WHERE d.refclassid = 'pg_extension'::regclass
          AND d.refobjid = e.oid) AS object_count,
       e.extnamespace::regnamespace AS schema
FROM pg_extension e
ORDER BY e.extname;

-- Check which extensions use untrusted languages
SELECT e.extname,
       l.lanname
FROM pg_extension e
JOIN pg_depend d ON d.refobjid = e.oid AND d.deptype = 'e'
JOIN pg_language l ON l.oid = d.objid
WHERE l.lanpltrusted = false;
Output
extname | extversion | object_count | schema
-----------+------------+--------------+--------
plpgsql | 1.0 | 2 | pg_catalog
postgis | 3.3.2 | 412 | public
pgvector | 0.6.0 | 12 | public
(3 rows)
extname | lanname
---------+---------
(0 rows)
Senior Shortcut:
Run your extension audit quarterly. If an extension has more than 500 registered objects, it's touching too many system catalogs. That's a risk factor. Also, extensions in the public schema are harder to manage — specify a custom schema with CREATE EXTENSION ... SCHEMA extensions.
Key Takeaway
Extensions are power tools. Use them deliberately, audit them regularly, and never trust one you haven't tested against your specific workload.

pg_stat_statements: The One Extension You Install Before You Debug Anything

Stop guessing why your database is slow. pg_stat_statements is the only extension that gives you the actual numbers — query frequency, latency, I/O, temp file usage, and blocking time. It's a shared library that hooks into the executor to track every query's runtime statistics. Without it, you're flying blind.

Install it. Enable it. Query pg_stat_statements sorted by total_time descending. That's your hit list. Every normalization failure, every sequential scan on a million-row table, every forgetten index — this extension exposes them. Production postmortems start here. The catch? It must be loaded at cluster startup via shared_preload_libraries, not on the fly. Plan your restarts accordingly. Once it's in, you get zero-cost visibility into query performance. Every replica needs it too — same shared object, same version, or queries crash.

TopQueries.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
// io.thecodeforge — database tutorial

SELECT
  queryid,
  query,
  calls,
  total_exec_time / calls AS avg_ms,
  rows / calls AS avg_rows_per_call,
  shared_blks_hit::float / NULLIF(shared_blks_read + shared_blks_hit, 0) * 100 AS cache_hit_pct
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;
Output
queryid | query | calls | avg_ms | avg_rows_per_call | cache_hit_pct
-------------------+--------------------------------------------------------------------------------------------+-------+--------+-------------------+---------------
-4529321347267923 | SELECT * FROM orders WHERE status = $1 AND created_at < $2 LIMIT $3 | 48291 | 23.45 | 15 | 99.8
-3901809221384011 | UPDATE inventory SET count = count - $1 WHERE product_id = $2 AND count >= $1 | 19204 | 18.12 | 1 | 97.2
Production Trap:
Resetting pg_stat_statements with pg_stat_statements_reset() wipes all history. Do this during maintenance windows only — you lose trend data for capacity planning.
Key Takeaway
Don't tune what you can't measure. pg_stat_statements is your first and last tool for query performance.

hstore: The Key-Value Extension You Never Knew You Needed (Until You Don't)

hstore stores arbitrary key-value pairs in a single column. No schema changes. No null columns piling up. It's a hash map inside PostgreSQL — => syntax, ? for existence checks, #> for path access. Performance is solid because it's backed by C-level hash tables.

Why use it? When you need flexible attributes that don't justify a full EAV table or a JSONB column. Product metadata, A/B test flags, user preferences — hstore handles them with less overhead than JSONB for simple string values. No parsing cost, no nested object complexity. The killer feature? GIN indexes on ? and @> operators make lookups fast even across millions of rows. But do not store values over 1KB here — you'll bloat indexes. And forget about nested data; hstore is flat by design. That's the trade-off: raw speed for limited structure. Use it where JSONB is overkill.

ProductAttributes.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// io.thecodeforge — database tutorial

CREATE EXTENSION IF NOT EXISTS hstore;

CREATE TABLE products (
  id SERIAL PRIMARY KEY,
  name TEXT NOT NULL,
  attributes hstore
);

CREATE INDEX idx_products_attrs ON products USING GIN(attributes);

INSERT INTO products (name, attributes)
VALUES
  ('Widget', 'color => red, weight => 2.5kg, in_stock => true'::hstore),
  ('Gadget', 'color => blue, material => steel, warranty => 5 years'::hstore);

SELECT name, attributes->'color' AS color
FROM products
WHERE attributes ? 'warranty';
Output
name | color
--------+-------
Gadget | blue
(1 row)
Senior Shortcut:
hstore keys are case-sensitive and stored as strings. Normalize them to lowercase in application code to avoid gotchas during lookups.
Key Takeaway
hstore is for flat, fast, untyped key-value bags. If you need nesting, use JSONB. If you need speed on string lookups, hstore wins.

Sharp H2

PostgreSQL extensions are not magic—they are shared objects loaded into the backend process, but their real power comes from the design choices you make. The three pillars we've explored—pgvector for AI embeddings, PostGIS for spatial data, pg_partman for time-series partitioning—demonstrate how extensions can solve specific problems without bloating your database. But the lesson isn't just about these tools. It's about understanding when an extension is a shortcut to production stability, not a toy. The defensive model we built around security—stricter search_path, explicit schema grants, and revoking dangerous functions—protects you from the very power extensions provide. Extensions should always be deployed via idempotent migration scripts, not ad-hoc SQL. They must be version-locked across replicas, tested in staging, and never left to drift. The future of PostgreSQL is an ecosystem of purpose-built extensions, but only disciplined teams will harness them safely. Remember: an extension is a contract with your database, not a temporary fix. Treat it like one.

verify_extensions.sqlSQL
1
2
3
4
5
6
7
// io.thecodeforge — database tutorial
-- Final check: extension versions across all replicas
SELECT e.extname, e.extversion, 
       current_timestamp AS checked_at,
       pg_is_in_recovery() AS is_replica
FROM pg_extension e
ORDER BY e.extname;
Output
extname | extversion | checked_at | is_replica
--------------+------------+---------------------+-----------
pg_partman | 5.2.1 | 2025-01-15 10:30:00 | f
pg_stat_stat | 1.10 | 2025-01-15 10:30:00 | f
pgvector | 0.7.4 | 2025-01-15 10:30:00 | f
postgis | 3.4.2 | 2025-01-15 10:30:00 | f
Production Trap:
Never run ALTER EXTENSION UPDATE on a primary without first upgrading every replica. A version mismatch between replicas causes query failures when the extension's internal ABI changes—and there's no rollback mid-flight.
Key Takeaway
Extensions must be version-locked across all replicas and deployed through migration scripts, never ad-hoc.

Sharp H2

Before you ship that next extension, ask yourself: does the problem genuinely require a database-native solution, or is it a caching layer, an external service, or a simpler SQL construct in disguise? The most common production failures come from extensions that do too much—bloating shared_buffers, locking catalog tables during CREATE EXTENSION, or silently consuming disk with TOAST tables. We've seen teams install pg_partman without setting retention policies, resulting in 2TB partition tables that were never cleaned up. The rule of thumb: use extensions for what they uniquely provide—indexing algorithms (pgvector's IVFFlat), spatial types (PostGIS's GEOMETRY), or automated partitioning (pg_partman's time-based triggers). For everything else—rate limiting, complex business logic, or HTTP calls—use application code. Extensions add surface area: each one is a potential vulnerability (CVE history), a performance overhead during writes, and a migration blocker when upgrading PostgreSQL major versions. The disciplined path is to audit your current extensions quarterly, remove unused ones, and always test upgrades in a pre-prod environment with production-like data volumes.

audit_extensions.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
// io.thecodeforge — database tutorial
-- Weekly audit: list extensions with unsafe permissions
SELECT e.extname, e.extversion,
       array_agg(DISTINCT n.nspname) AS schemas,
       count(p.oid) AS total_procs
FROM pg_extension e
JOIN pg_namespace n ON n.oid = e.extnamespace
LEFT JOIN pg_proc p ON p.pronamespace = n.oid
GROUP BY e.extname, e.extversion
HAVING count(p.oid) > 100
ORDER BY total_procs DESC;
Output
extname | extversion | schemas | total_procs
----------+------------+------------------+-------------
postgis | 3.4.2 | {public,postgis} | 487
pgvector | 0.7.4 | {public} | 12
Production Trap:
PostGIS installs hundreds of functions into public by default—each one a potential vector for SQL injection if you use dynamic queries. Always install PostGIS into a dedicated schema (CREATE SCHEMA postgis) and set search_path per-role, not per-database.
Key Takeaway
Audit extensions quarterly: remove unused ones, cap complexity, and always install into isolated schemas to limit security exposure.
● Production incidentPOST-MORTEMseverity: high

Replica Crash After Installing pg_stat_statements

Symptom
After running CREATE EXTENSION pg_stat_statements on the primary, the streaming replica disconnected and its logs showed: 'ERROR: could not load library /usr/lib/postgresql/16/lib/pg_stat_statements.so: No such file or directory'. Replication lag spiked to infinity.
Assumption
The team assumed extension SQL scripts are replicated and applied on the standby automatically, and that the shared library would be present because they installed it on all nodes.
Root cause
The key insight: CREATE EXTENSION is replicated via WAL. When the standby replays the WAL record, it tries to load the .so file via dlopen(). If that file doesn't exist on the standby's filesystem, the standby backend crashes and the WAL replay stops. The team had only installed the postgresql-16-pg-stat-statements package on the primary.
Fix
Install the extension OS package on every replica and the primary before running CREATE EXTENSION. For existing clusters with the package missing on replicas, install the package and restart the standby (or use pg_ctl promote if it's already broken).
Key lesson
  • Always install extension OS packages on all cluster nodes before running CREATE EXTENSION on the primary.
  • Treat the .so file deployment as a prerequisite in your runbook — not an afterthought.
  • After installing a new extension, verify replication health with SELECT pg_is_in_recovery() and check pg_stat_replication for lag.
Production debug guideSymptom → Action guide for the four most common extension failures in production4 entries
Symptom · 01
CREATE EXTENSION succeeds but extension functions return empty results or no rows
Fix
Check if the extension requires shared_preload_libraries. Verify by SELECT name, pending_restart FROM pg_settings WHERE name = 'shared_preload_libraries'. If missing, add it, restart, then re-test.
Symptom · 02
ALTER EXTENSION UPDATE fails with 'could not open extension control file'
Fix
The OS package hasn't been updated. Run apt update && apt upgrade on all nodes first, then re-run ALTER EXTENSION. Verify new .sql files in SHAREDIR/extension/.
Symptom · 03
Search_path errors: function not found even though extension is installed
Fix
Run SELECT extname, nspname FROM pg_extension JOIN pg_namespace ON extnamespace = pg_namespace.oid. Add the extension's schema to your search_path with SET search_path TO public, <ext_schema>;.
Symptom · 04
Replica crashes after CREATE EXTENSION on primary
Fix
The .so file is missing on the standby. Install the OS package on all replicas. If the standby is already broken, reinitialize it from the primary using pg_basebackup.
★ Quick Extension Debug Cheat SheetUse these commands when an extension misbehaves — no theory, just the fix.
Extension doesn't show data (empty views)
Immediate action
Check shared_preload_libraries
Commands
SELECT name, setting, pending_restart FROM pg_settings WHERE name = 'shared_preload_libraries';
If missing: ALTER SYSTEM SET shared_preload_libraries = 'pg_stat_statements'; then restart cluster.
Fix now
Restart the cluster after ALTER SYSTEM SET. Verify with SELECT calls FROM pg_stat_statements LIMIT 1;
Extension upgrade fails+
Immediate action
Check package version vs installed version
Commands
SELECT name, installed_version, default_version FROM pg_available_extensions WHERE name = 'your_ext';
apt list --upgradable | grep postgresql
Fix now
apt upgrade on all nodes, then ALTER EXTENSION your_ext UPDATE;
Function not found error+
Immediate action
Check search_path and schema
Commands
SHOW search_path;
SELECT extname, nspname FROM pg_extension JOIN pg_namespace ON extnamespace = pg_namespace.oid;
Fix now
SET search_path TO public, <extension_schema>; or qualify function calls with schema.
Replica crash after extension install+
Immediate action
Check standby logs for missing library error
Commands
On standby: tail -100 /var/log/postgresql/postgresql-16-main.log | grep 'could not load library'
Verify .so file on standby: ls /usr/lib/postgresql/16/lib/ | grep <extension>
Fix now
Install OS package on standby, then restart standby or reinitialize.
IVFFlat vs HNSW Index in pgvector
AspectIVFFlat Index (pgvector)HNSW Index (pgvector)
Build timeFast — minutes for millions of rowsSlow — can take hours for millions of rows
Query recall at k=10~90-95% (tunable via probes)~97-99% (tunable via ef_search)
Memory during buildLow — uses work_memHigh — uses maintenance_work_mem per connection
Memory at query timeLow — index is disk-residentHigher — upper graph layers cached in shared_buffers
Requires row count for buildYes — must INSERT data first, then CREATE INDEXNo — can build on empty table, rows added incrementally
Index size on diskSmallerLarger (~2-3x IVFFlat for same dataset)
Best use caseBatch workloads, frequently re-built indexesProduction similarity search requiring high recall
Available since pgvector0.4.00.5.0 (late 2023)

Key takeaways

1
pg_depend is the source of truth
every object an extension creates is registered there, which is why DROP EXTENSION CASCADE is safe and why you can't accidentally lose extension-owned objects without explicitly dropping the extension.
2
shared_preload_libraries is a silent failure trap
if an extension requires it and it's missing, CREATE EXTENSION works but the extension does nothing, with no error raised. Always verify with a functional test, not just a successful DDL.
3
The OS package upgrade and ALTER EXTENSION UPDATE are two separate, ordered steps
skipping either one leaves your cluster in an inconsistent state where the database version and the binary on disk disagree.
4
HNSW indexes in pgvector give you near-exact recall but cost significant memory at build time and larger index size on disk
IVFFlat is faster to build and cheaper, making it the right choice for workloads where you re-index frequently or have tight memory budgets.
5
Revoke PUBLIC access on extension functions immediately after install
especially reset/delete functions — to prevent accidental data loss or performance degradation.

Common mistakes to avoid

4 patterns
×

Forgetting shared_preload_libraries for extensions that need it

Symptom
CREATE EXTENSION succeeds with no error, but pg_stat_statements shows empty results or pg_prewarm does nothing. No warning is raised.
Fix
Check the extension docs for 'requires preloading', add the extension name to shared_preload_libraries via ALTER SYSTEM SET, then restart the cluster. Verify with SELECT * FROM pg_settings WHERE name = 'shared_preload_libraries'.
×

Running ALTER EXTENSION UPDATE without first updating the OS package

Symptom
ALTER EXTENSION name UPDATE fails with 'could not open extension control file' or executes but the new .so file is missing, causing function calls to crash with 'invalid memory alloc request'.
Fix
Always follow the order: (1) apt/yum upgrade the Postgres extension package on every node, (2) then run ALTER EXTENSION UPDATE in SQL. Treat the two steps as an atomic deployment unit in your runbook.
×

Installing extensions into the public schema and granting broad access

Symptom
Any user with CONNECT privilege can call dangerous functions like pg_stat_statements_reset(), wiping monitoring data, or PostGIS functions that are computationally expensive.
Fix
Install relocatable extensions into a dedicated schema (CREATE EXTENSION postgis SCHEMA geo;), set search_path explicitly for application roles, and use REVOKE ALL ON FUNCTION ... FROM PUBLIC followed by targeted GRANTs. For non-relocatable extensions, use REVOKE immediately after CREATE EXTENSION.
×

Building an HNSW index without adjusting maintenance_work_mem

Symptom
Index build fails with 'out of memory' or runs extremely slowly because the default maintenance_work_mem (64MB) is too low for large datasets.
Fix
Set maintenance_work_mem to at least 2GB in the session before building the HNSW index. Monitor build progress with pg_stat_progress_create_index. After build, reset maintenance_work_mem to default.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
What's the difference between CREATE EXTENSION and simply running the ex...
Q02SENIOR
If a PostgreSQL replica keeps crashing after you installed a new extensi...
Q03SENIOR
You've upgraded the PostGIS OS package from 3.3 to 3.4 on all nodes. A c...
Q01 of 03SENIOR

What's the difference between CREATE EXTENSION and simply running the extension's SQL script manually — and why does it matter for schema management?

ANSWER
CREATE EXTENSION automates three things: (1) reads the .control file to determine version, dependencies, and schema, (2) runs the SQL installation script inside a transaction so failure rolls back everything, and (3) registers all created objects in pg_depend with deptype 'e'. This means DROP EXTENSION CASCADE can safely remove all extension-owned objects. Running the SQL script manually would create objects but Postgres wouldn't associate them with an extension, so DROP EXTENSION wouldn't clean them up — you'd need to track and drop each object individually. For schema management, extension ownership guarantees that upgrades and removals are safe and reversible.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
Can I install PostgreSQL extensions without superuser privileges?
02
Does installing an extension affect PostgreSQL performance even if I never use it?
03
What happens to my data if I DROP EXTENSION with CASCADE?
04
How do I move an extension to a different schema after installation?
N
Naren Founder & Principal Engineer

20+ years shipping high-throughput database systems. Everything here is grounded in real deployments.

Follow
Verified
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
🔥

That's MySQL & PostgreSQL. Mark it forged?

12 min read · try the examples if you haven't

Previous
MySQL Performance Tuning
8 / 13 · MySQL & PostgreSQL
Next
PostgreSQL Triggers