PostgreSQL Advanced
Extensions Ecosystem
Section titled “Extensions Ecosystem”Extensions add functionality to PostgreSQL through a well-defined API. They run in the same process As the server and have access to the same data, making them powerful but also a trust boundary.
Installing Extensions
Section titled “Installing Extensions”-- Available extensionsSELECT * FROM pg_available_extensions;
-- Install an extensionCREATE EXTENSION IF NOT EXISTS postgis;CREATE EXTENSION IF NOT EXISTS pg_trgm;CREATE EXTENSION IF NOT EXISTS pgcrypto;CREATE EXTENSION IF NOT EXISTS hstore;CREATE EXTENSION IF NOT EXISTS uuid-ossp;CREATE EXTENSION IF NOT EXISTS citext;CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
-- Check installed extensionsSELECT * FROM pg_extension;PostGIS
Section titled “PostGIS”PostGIS adds geographic data types and spatial functions:
CREATE EXTENSION IF NOT EXISTS postgis;
CREATE TABLE locations ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, geom GEOMETRY(POINT, 4326) NOT NULL -- WGS84 (GPS coordinates));
CREATE INDEX idx_locations_geom ON locations USING GIST (geom);
-- Insert a point (longitude, latitude)INSERT INTO locations (name, geom)VALUES ("San Francisco', ST_SetSRID(ST_MakePoint(-122.4194, 37.7749), 4326));
-- Find locations within 10km of a pointSELECT name, ST_Distance(geom, ST_SetSRID(ST_MakePoint(-122.4, 37.77), 4326)) AS distance_metersFROM locationsWHERE ST_DWithin(geom, ST_SetSRID(ST_MakePoint(-122.4, 37.77), 4326), 10000)ORDER BY distance_meters;pg_trgm
Section titled “pg_trgm”Trigram-based similarity and fuzzy matching:
CREATE EXTENSION IF NOT EXISTS pg_trgm;
-- GIN trigram index for fast ILIKECREATE INDEX idx_users_name_trgm ON users USING GIN (name gin_trgm_ops);
-- Fast case-insensitive partial matchSELECT * FROM users WHERE name ILIKE '%john%';
-- Similarity search (0 to 1, where 1 is exact match)SELECT name, similarity(name, 'John Smith') AS simFROM usersWHERE name % 'John Smith'ORDER BY sim DESC;
-- Adjust similarity thresholdSET pg_trgm.similarity_threshold = 0.3;pgcrypto
Section titled “pgcrypto”Cryptographic functions:
CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- Generate UUIDsSELECT gen_random_uuid();-- Returns: 550e8400-e29b-41d4-a716-446655440000
-- Hash functionsSELECT digest('password', 'sha256');SELECT crypt('password', gen_salt('bf'));
-- Verify passwordSELECT (crypt('password', stored_hash) = stored_hash) AS is_validFROM users WHERE email = 'user@example.com';
-- PGP encryptionSELECT pgp_sym_encrypt('secret data', 'encryption_key');SELECT pgp_sym_decrypt(encrypted_column, 'encryption_key');hstore
Section titled “hstore”Key-value pairs within a single column:
CREATE EXTENSION IF NOT EXISTS hstore;
CREATE TABLE products ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, attributes HSTORE);
INSERT INTO products (name, attributes)VALUES ('Widget', 'color => "red", weight => "2.5", material => "steel"');
-- Query hstore fieldsSELECT name FROM products WHERE attributes -> 'color' = 'red';SELECT name, attributes -> 'weight' AS weight FROM products;
-- GIN index for hstore containmentCREATE INDEX idx_products_attrs ON products USING GIN (attributes);
-- Containment queriesSELECT * FROM products WHERE attributes @> 'color => "red"';SELECT * FROM products WHERE attributes ? 'weight'; -- has key 'weight'uuid-ossp
Section titled “uuid-ossp”CREATE EXTENSION IF NOT EXISTS uuid-ossp;
-- UUID v1 (MAC address + timestamp)SELECT uuid_generate_v1();
-- UUID v4 (random)SELECT uuid_generate_v4();
-- UUID v5 (SHA-1 hash of namespace + name, deterministic)SELECT uuid_generate_v5(uuid_ns_dns(), 'example.com');citext
Section titled “citext”Case-insensitive text:
CREATE EXTENSION IF NOT EXISTS citext;
CREATE TABLE accounts ( username CITEXT PRIMARY KEY, email CITEXT NOT NULL);
-- Both insertions fail (case-insensitive uniqueness)INSERT INTO accounts VALUES ('Alice', 'alice@example.com');INSERT INTO accounts VALUES ('alice', 'alice@other.com');-- ERROR: duplicate key value violates unique constraintpg_stat_statements
Section titled “pg_stat_statements”-- Must be loaded at server start-- shared_preload_libraries = 'pg_stat_statements'
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
-- Top queries by total timeSELECT query, calls, total_exec_time / 1000 AS total_ms, mean_exec_time / 1000 AS avg_msFROM pg_stat_statementsORDER BY total_exec_time DESCLIMIT 10;Logical Replication
Section titled “Logical Replication”Physical vs Logical Replication
Section titled “Physical vs Logical Replication”| Aspect | Physical Replication | Logical Replication |
|---|---|---|
| What replicates | Entire WAL (all databases, all tables) | Selected tables (per-publication) |
| Granularity | Database level (entire cluster) | Table level (per publication/subscription) |
| Cross-version | Same major version only | Can replicate between major versions |
| Write on replica | No (read-only) | Yes (subscription tables are writable) |
| Use case | High availability, disaster recovery | Data sharing, partial replication, CDC |
Setting Up Logical Replication
Section titled “Setting Up Logical Replication”Publisher (source):
-- Set wal_level = logical in postgresql.conf and restart-- wal_level = logical
CREATE PUBLICATION pub_orders FOR TABLE orders;CREATE PUBLICATION pub_all_tables FOR ALL TABLES;
-- Publication with filterCREATE PUBLICATION pub_active_orders FOR TABLE orders WHERE (status = 'active' AND created_at >= '2024-01-01');Subscriber (target):
CREATE SUBSCRIPTION sub_orders CONNECTION 'host=publisher.example.com dbname=mydb user=replicator' PUBLICATION pub_orders WITH (create_slot = true, slot_name = 'sub_orders_slot');
-- Verify subscription statusSELECT * FROM pg_stat_subscription;Conflict Resolution
Section titled “Conflict Resolution”On the subscriber, conflicts can occur when the subscription table has local modifications that Conflict with incoming replication changes:
-- Configure conflict resolutionALTER SUBSCRIPTION sub_orders SET (slot_name = 'sub_orders_slot');
-- Handle conflicts (check pg_stat_subscription_stats)SELECT subname, slot_name, sync_error_count, apply_error_countFROM pg_stat_subscription_stats;
-- Common conflicts:-- 1. insert_duplicate_key: row already exists on subscriber-- 2. update_conflict: row version mismatch-- Solutions: skip errors, or use trigger-based conflict resolutionALTER SUBSCRIPTION sub_orders SKIP (ALTER TABLE orders REPLICA IDENTITY FULL);Replication Identity
Section titled “Replication Identity”Logical replication needs to know which column(s) uniquely identify a row:
-- Default: primary keyALTER TABLE orders REPLICA IDENTITY DEFAULT;
-- Use a unique indexALTER TABLE orders REPLICA IDENTITY USING INDEX idx_orders_order_id;
-- Full row comparison (no unique index available, slower)ALTER TABLE orders REPLICA IDENTITY FULL;
-- No row identity (deletes are not replicated)ALTER TABLE orders REPLICA IDENTITY NOTHING;Foreign Data Wrappers (FDW)
Section titled “Foreign Data Wrappers (FDW)”postgres_fdw
Section titled “postgres_fdw”Query remote PostgreSQL servers as if they were local tables:
CREATE EXTENSION IF NOT EXISTS postgres_fdw;
-- Create the foreign serverCREATE SERVER remote_db FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host 'remote.example.com', port '5432', dbname 'analytics');
-- Create user mapping (local user → remote user)CREATE USER MAPPING FOR local_app SERVER remote_db OPTIONS (user 'analytics_user', password 'secret');
-- Import remote tablesIMPORT FOREIGN SCHEMA public LIMIT TO (events, users) FROM SERVER remote_db INTO public;
-- Or create individual foreign tablesCREATE FOREIGN TABLE remote_events ( event_id BIGINT, event_type TEXT, payload JSONB, created_at TIMESTAMPTZ) SERVER remote_db OPTIONS (schema_name 'public', table_name 'events');-- Query the remote table (PostgreSQL pushes down WHERE and LIMIT)SELECT event_type, COUNT(*) FROM remote_eventsWHERE created_at >= '2024-01-01'GROUP BY event_type;
-- Join local and remote tablesSELECT u.name, COUNT(r.event_id) AS event_countFROM users uJOIN remote_events r ON u.user_id = r.user_idWHERE r.created_at >= '2024-01-01'GROUP BY u.name;Pg_cron requires shared_preload_libraries = 'pg_cron' and a PostgreSQL restart. Jobs run in the Context of the database where pg_cron is installed. Cross-database scheduling is not supported.
pg_background
Section titled “pg_background”Pg_background runs commands in the background, returning control to the client immediately:
CREATE EXTENSION IF NOT EXISTS pg_background;
-- Run a long-running vacuum in the backgroundSELECT pg_background.launch('VACUUM ANALYZE orders');
-- Check background worker statusSELECT * FROM pg_background.result;pgBouncer Configuration Deep-Dive
Section titled “pgBouncer Configuration Deep-Dive”Advanced Configuration
Section titled “Advanced Configuration”; pgbouncer.ini
[databases]; Override per-database settingsmydb = host=127.0.0.1 port=5432 dbname=mydb pool_size=10analytics = host=analytics-db port=5432 dbname=analytics pool_mode=session
[pgbouncer]; Pool modespool_mode = transaction
; Connection limitsmax_client_conn = 1000default_pool_size = 20min_pool_size = 5 ; keep minimum connections warmreserve_pool_size = 5 ; extra connections for spikesreserve_pool_timeout = 3 ; seconds to wait for reserve pool
; Timeoutsserver_idle_timeout = 600 ; close idle server connections after 10 minserver_lifetime = 3600 ; recycle server connections after 1 hourclient_idle_timeout = 0 ; do not disconnect idle clientsclient_login_timeout = 60 ; login must complete within 60s
; Server connection settingsserver_connect_timeout = 15server_check_delay = 30 ; health check intervalserver_check_query = SELECT 1server_reset_query = DISCARD ALL
; Logginglog_connections = 1log_disconnections = 1log_pooler_errors = 1verbose = 1
; Admin interfaceadmin_users = adminstats_period = 60Monitoring PgBouncer
Section titled “Monitoring PgBouncer”-- Connect to PgBouncer admin consolepsql -h 127.0.0.1 -p 6432 -U admin pgbouncer
-- Show pool statisticsSHOW POOLS;
-- Show databasesSHOW DATABASES;
-- Show clientsSHOW CLIENTS;
-- Show server connectionsSHOW SERVERS;
-- Show active queriesSHOW STATS;
-- Stats per databaseSHOW STATS_STATS;
-- List configured databasesSHOW LISTS;Logical Decoding and Change Data Capture
Section titled “Logical Decoding and Change Data Capture”Setting Up Logical Decoding
Section titled “Setting Up Logical Decoding”-- Ensure wal_level = logical (requires restart)-- wal_level = logical
-- Create a replication slot for logical decodingSELECT pg_create_logical_replication_slot('my_slot', 'pgoutput');
-- Read changesSELECT * FROM pg_logical_slot_peek_changes('my_slot', NULL, NULL);
-- Consume changes (advances the slot)SELECT * FROM pg_logical_slot_get_changes('my_slot', NULL, NULL);
-- Drop the slotSELECT pg_drop_replication_slot('my_slot');CDC with Debezium
Section titled “CDC with Debezium”Debezium captures row-level changes from PostgreSQL WAL and publishes them to Kafka:
Architecture: PostgreSQL → WAL → Debezium Connector → Kafka → Consumers
Debezium reads WAL using logical replication slots.It publishes change events as JSON/Avro to Kafka topics.Each table gets its own topic.Events include: before/after images, operation type, transaction metadata.