PostgreSQL Advanced Features
Postgres has been quietly absorbing features for thirty years. In 2026 it's defensible to argue you should reach for non-Postgres datastores only when you have specific reasons, because Postgres can do most of what those datastores do — well enough that the operational simplicity of one system wins.
This page is the features that surprise people, ranked by how often they replace another tool.
JSONB
JSONB stores binary-encoded JSON with index support and a query language. Most "we need a document database" use cases are served by JSONB without leaving Postgres.
```sql
CREATE TABLE events (
id BIGSERIAL PRIMARY KEY,
occurred_at TIMESTAMPTZ NOT NULL,
payload JSONB NOT NULL
);
-- Path queries
SELECT * FROM events WHERE payload @> '{"type": "click", "user_id": 42}';
SELECT * FROM events WHERE payload->>'session_id' = 'abc-123';
-- GIN index on the whole JSONB (large but flexible)
CREATE INDEX idx_events_payload ON events USING GIN (payload);
-- GIN index for containment queries only (smaller)
CREATE INDEX idx_events_payload ON events USING GIN (payload jsonb_path_ops);
-- Functional index for a specific path
CREATE INDEX idx_events_user ON events ((payload->>'user_id'));
```
When to use:
- **Schema-flexible event logs** where each event type has different fields.
- **Document-like data** that's queried mostly by path.
- **Mostly-structured data with a few flexible fields** — most columns are typed; one `metadata` JSONB.
When not to use:
- Data with a stable schema. Use real columns. JSONB is slower, harder to constrain, and harder to query.
- High-frequency updates of nested fields. JSONB is rewritten as a whole; partial updates are expensive.
See [JsonbInPostgresql]() for depth.
LISTEN / NOTIFY
Lightweight pub-sub built into Postgres. Producers send notifications:
```sql
SELECT pg_notify('order_events', '{"id": 42, "status": "shipped"}');
```
Subscribers listen:
```sql
LISTEN order_events;
-- Connection now receives async notifications
```
Use cases:
- Cache invalidation across application instances.
- Real-time updates for SPAs (combined with a WebSocket bridge).
- Triggering background work without a separate queue.
Limits:
- Payload max 8000 bytes.
- No persistence — if no listener is connected, the notification is lost.
- Not a replacement for Kafka or RabbitMQ for durable messaging.
For "tell other instances something changed" within a single application, LISTEN/NOTIFY is dramatically simpler than running a message broker.
pgvector
Native vector search via the `pgvector` extension. Adds `VECTOR(n)` type, IVFFlat and HNSW indexes, and operators (`<->` for L2, `<#>` for negative inner product, `<=>` for cosine).
```sql
CREATE EXTENSION vector;
CREATE TABLE chunks (
id BIGSERIAL PRIMARY KEY,
document_id BIGINT REFERENCES documents(id),
content TEXT NOT NULL,
embedding VECTOR(1536) NOT NULL
);
CREATE INDEX idx_chunks_embedding ON chunks
USING hnsw (embedding vector_cosine_ops);
-- Top-10 most similar
SELECT id, content, 1 - (embedding <=> $1) AS similarity
FROM chunks
ORDER BY embedding <=>$1
LIMIT 10;
```
For most teams in 2026, pgvector is the right vector database. Tens of millions of vectors, sub-100ms recall, no separate operational system. See [VectorDatabases]() for the comparison.
Common Table Expressions (CTEs)
CTEs are SQL's structuring tool. Use them generously.
```sql
WITH active_users AS (
SELECT id, email FROM users WHERE deleted_at IS NULL
),
recent_orders AS (
SELECT user_id, total FROM orders
WHERE created_at > NOW() - INTERVAL '30 days'
)
SELECT u.email, COALESCE(SUM(o.total), 0) AS spent
FROM active_users u
LEFT JOIN recent_orders o ON o.user_id = u.id
GROUP BY u.id, u.email;
```
Recursive CTEs handle hierarchies (org charts, comment trees) and graph traversals:
```sql
WITH RECURSIVE descendants AS (
SELECT id, parent_id, name FROM categories WHERE id = 5
UNION ALL
SELECT c.id, c.parent_id, c.name
FROM categories c JOIN descendants d ON c.parent_id = d.id
)
SELECT * FROM descendants;
```
Pre-PG12 CTEs were optimisation fences. PG12+ inlined them, so CTEs are now a structuring tool with no performance cost vs subqueries. Use them.
Window functions
```sql
SELECT
user_id,
order_id,
total,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at) AS order_seq,
SUM(total) OVER (PARTITION BY user_id ORDER BY created_at) AS running_total,
LAG(total) OVER (PARTITION BY user_id ORDER BY created_at) AS prev_order_total
FROM orders;
```
Solves "running totals," "rank within group," "time since last event," and similar queries that would otherwise require self-joins.
Range types
Native types for ranges (numeric, date, timestamp). With `EXCLUDE` constraints, you get conflict-free booking systems:
```sql
CREATE TABLE bookings (
id BIGSERIAL PRIMARY KEY,
room_id BIGINT REFERENCES rooms(id),
period TSTZRANGE NOT NULL,
EXCLUDE USING gist (room_id WITH =, period WITH &&)
);
```
This is one constraint that prevents any two bookings of the same room from overlapping in time. The database does the conflict detection; you don't.
Partitioning
Native declarative partitioning since PG10. Partition by range, list, or hash.
```sql
CREATE TABLE events (
id BIGINT,
occurred_at TIMESTAMPTZ NOT NULL,
-- ...
) PARTITION BY RANGE (occurred_at);
CREATE TABLE events_2026_q1 PARTITION OF events
FOR VALUES FROM ('2026-01-01') TO ('2026-04-01');
```
Use cases:
- Time-series tables that grow forever — drop old partitions instead of `DELETE`.
- Multi-tenant tables where queries are mostly per-tenant — partition by `tenant_id` for query pruning.
- Massive tables where vacuum, indexing, and maintenance per-partition wins.
`pg_partman` automates partition creation/dropping for time-series. Worth the install on any production time-series workload.
Logical replication
Publish/subscribe between Postgres instances at the row level (not just byte-level streaming):
- Replicate a subset of tables to a reporting database.
- Migrate between major versions with minimal downtime (zero-downtime if you're careful).
- Cross-region read replicas with bandwidth control.
- Capture changes for downstream pipelines (alternative to Debezium for some use cases).
```sql
-- On primary
CREATE PUBLICATION my_pub FOR TABLE orders, users;
-- On replica
CREATE SUBSCRIPTION my_sub
CONNECTION 'host=primary user=replicator dbname=main'
PUBLICATION my_pub;
```
FDW (Foreign Data Wrappers)
Query other databases as if they were tables. `postgres_fdw` for cross-Postgres; `oracle_fdw`, `mysql_fdw`, `mongo_fdw`, `clickhouse_fdw` for others.
Use case: pulling data from a legacy system into reports without ETL. Less common in 2026 (data warehouses subsume this), but still useful for migrations.
Trigger-based history
Combined with `JSONB`, simple history table:
```sql
CREATE TABLE orders_history (
id BIGSERIAL PRIMARY KEY,
order_id BIGINT NOT NULL,
snapshot JSONB NOT NULL,
op TEXT NOT NULL CHECK (op IN ('insert','update','delete')),
changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE FUNCTION log_orders_history() RETURNS TRIGGER AS $$BEGIN
INSERT INTO orders_history (order_id, snapshot, op)
VALUES (COALESCE(NEW.id, OLD.id), to_jsonb(COALESCE(NEW, OLD)), TG_OP::TEXT);
RETURN COALESCE(NEW, OLD);
END;$$ LANGUAGE plpgsql;
CREATE TRIGGER orders_history_trigger
AFTER INSERT OR UPDATE OR DELETE ON orders
FOR EACH ROW EXECUTE FUNCTION log_orders_history();
```
Cheap, comprehensive, queryable. Adequate for most audit/history use cases without needing temporal-table machinery.
Other tools worth knowing
- **`generate_series`** for synthetic test data.
- **`array_agg` / `jsonb_agg`** for compact aggregations.
- **`tablefunc.crosstab`** for pivots.
- **`unnest`** for exploding arrays into rows.
- **GIST / GIN / BRIN / Bloom indexes** for non-B-tree indexing — see [DatabaseIndexingStrategies]().
- **PostGIS** for geographic data; mature and excellent.
- **TimescaleDB** for time-series at very large scale (extension on top of Postgres).
- **Citus** for horizontal scaling (extension turning Postgres into a distributed SQL database).
- **`pg_cron`** for scheduling jobs inside Postgres.
Where to leave Postgres
Despite all of the above, some workloads still want a different store:
- **Sub-millisecond key-value lookups at extreme scale** — Redis, Memcached.
- **Heavy time-series at billions of points** — InfluxDB, ClickHouse, TimescaleDB (Postgres extension).
- **OLAP / analytics over TB+** — Snowflake, BigQuery, ClickHouse, DuckDB.
- **Full-text search at extreme scale** — Elasticsearch / OpenSearch (Postgres has full-text but tops out earlier).
- **Document store with global distribution** — MongoDB, Cosmos DB.
- **Truly distributed SQL with multi-region writes** — CockroachDB, Spanner.
For most teams under "extreme" scale, the Postgres-everywhere posture saves operational cost. Adding a new datastore should require a specific reason.
Further reading
- [DatabaseDesign]() — schema choices that take advantage of these features
- [DatabaseIndexingStrategies]() — Postgres index types in depth
- [JsonbInPostgresql]() — JSONB specifically
- [VectorDatabases]() — pgvector vs alternatives
- [DatabasePartitioning]() — partitioning patterns