PostgreSQL logoPostgreSQLADVANCED

PostgreSQL Advanced Features

Advanced PostgreSQL cheat sheet with JSONB operations, window functions, CTEs, indexing strategies, and performance tuning examples.

15 min read
postgresqldatabaseperformanceoptimizationjsonbwindow-functionsadministrationmonitoring

New to SQL? Start Here First!

This sheet covers PostgreSQL-specific advanced features. If you're new to SQL or need a refresher on basic SQL commands, we recommend starting with our SQL fundamentals sheet first.

Start with SQL Fundamentals

Sign in to mark items as known and track your progress.

Sign in

PostgreSQL Data Types

Unique and powerful data types in PostgreSQL

Common Data Types

Frequently used PostgreSQL data types

📄 Codesql
-- Numeric types
INTEGER              -- 4-byte integer
BIGINT               -- 8-byte integer  
SERIAL               -- Auto-incrementing integer
NUMERIC(10,2)        -- Exact decimal
REAL / FLOAT         -- Floating point

-- String types
VARCHAR(255)         -- Variable length
TEXT                 -- Unlimited length
CHAR(10)            -- Fixed length

-- Date/Time types
DATE                -- Date only
TIME                -- Time only
TIMESTAMP           -- Date and time
TIMESTAMPTZ         -- With timezone
INTERVAL            -- Time duration

-- Boolean
BOOLEAN             -- TRUE/FALSE/NULL

-- DETAILED_TAB:
-- Type casting examples
SELECT 
  '123'::INTEGER,                    -- Cast to integer
  '2024-01-01'::DATE,               -- Cast to date
  1::BOOLEAN,                        -- 1 becomes TRUE
  CAST('123.45' AS NUMERIC),        -- SQL standard casting
  '1 hour 30 minutes'::INTERVAL     -- Interval type
🟢 Essential - Know your data types for efficient storage
💡 Use TEXT instead of VARCHAR when length is unknown
📌 SERIAL is PostgreSQL's AUTO_INCREMENT
⚡ TIMESTAMPTZ is preferred over TIMESTAMP
🔗 Related: pg_typeof() to check data types
datatypesbasics

Arrays

Store multiple values in a single column

📄 Codesql
-- Create table with arrays
CREATE TABLE products (
  id SERIAL PRIMARY KEY,
  name TEXT,
  tags TEXT[],           -- Array of text
  prices INTEGER[]       -- Array of integers
);

-- Insert arrays
INSERT INTO products (name, tags, prices) VALUES
  ('Laptop', ARRAY['electronics', 'computers'], ARRAY[999, 1299]),
  ('Book', '{"education", "programming"}', '{15, 20, 25}');

-- Query arrays
SELECT * FROM products 
WHERE 'electronics' = ANY(tags);     -- Contains value

SELECT * FROM products
WHERE tags @> ARRAY['computers'];    -- Contains all

-- DETAILED_TAB:
-- Array operations
SELECT 
  array_length(tags, 1),              -- Array length
  tags[1],                            -- First element (1-indexed!)
  array_append(tags, 'new'),          -- Add element
  array_remove(tags, 'old'),          -- Remove element
  unnest(tags)                        -- Expand to rows
FROM products;

-- Array aggregation
SELECT 
  category,
  array_agg(DISTINCT tag) AS all_tags
FROM product_tags
GROUP BY category;
💡 Arrays are 1-indexed in PostgreSQL, not 0-indexed
📌 Use ANY() and ALL() for array comparisons
⚡ Arrays can hurt performance if overused
⚠️ Consider normalization for complex array queries
🔗 Related: unnest() to convert array to rows
arraysdatatypes

JSONB

Store and query JSON data efficiently

📄 Codesql
-- Create table with JSONB
CREATE TABLE events (
  id SERIAL PRIMARY KEY,
  data JSONB NOT NULL
);

-- Insert JSON data
INSERT INTO events (data) VALUES
  ('{"user": "john", "action": "login", "timestamp": "2024-01-01"}'),
  ('{"user": "jane", "action": "purchase", "items": ["book", "pen"]}');

-- Query JSON fields
SELECT data->>'user' AS username       -- Get as text
FROM events
WHERE data->>'action' = 'login';

SELECT data->'items'->0                -- Get first array item
FROM events
WHERE data ? 'items';                  -- Has key

-- DETAILED_TAB:
-- Advanced JSONB operations
-- Update JSON fields
UPDATE events
SET data = jsonb_set(data, '{status}', '"completed"')
WHERE id = 1;

-- Query nested JSON
SELECT * FROM events
WHERE data @> '{"user": "john"}';      -- Contains

-- JSON aggregation
SELECT jsonb_agg(data) AS all_events
FROM events
WHERE data->>'action' = 'purchase';

-- Create index on JSON field
CREATE INDEX idx_user ON events((data->>'user'));
🟢 Essential - JSONB is perfect for flexible schemas
💡 Use JSONB not JSON for better performance
📌 -> returns JSON, ->> returns text
⚡ Index JSONB fields for fast queries
🔗 Related: jsonb_set(), jsonb_insert() for updates
jsonjsonbnosql

UUID & Special Types

Unique identifiers and specialized data types

📄 Codesql
-- Enable UUID extension
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

-- Create table with UUID
CREATE TABLE users (
  id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
  email CITEXT UNIQUE,              -- Case-insensitive text
  ip_address INET,                  -- IP address
  mac_address MACADDR,              -- MAC address
  price MONEY,                       -- Currency
  location POINT                     -- Geometric point
);

-- Insert with special types
INSERT INTO users VALUES
  (uuid_generate_v4(), 
   'John@Example.com',              -- CITEXT matches any case
   '192.168.1.1',
   '08:00:27:1e:4f:3a',
   '$19.99',
   point(40.7128, -74.0060));       -- NYC coordinates

-- DETAILED_TAB:
-- Range types
CREATE TABLE bookings (
  id SERIAL PRIMARY KEY,
  room_id INTEGER,
  during TSTZRANGE                  -- Timestamp range
);

-- Insert range
INSERT INTO bookings (room_id, during) VALUES
  (101, '[2024-01-01 14:00, 2024-01-01 16:00)');

-- Check overlap
SELECT * FROM bookings
WHERE during && '[2024-01-01 15:00, 2024-01-01 17:00)';
💡 UUID v4 is great for distributed systems
📌 CITEXT requires extension but very useful
⚡ INET type validates and stores IP efficiently
🔗 Related: PostGIS for advanced geographic types
⚠️ MONEY type has limitations, consider NUMERIC
uuidspecial-types

Window Functions

Perform calculations across rows without grouping

Ranking Functions

Assign ranks and row numbers to results

📄 Codesql
-- ROW_NUMBER - Sequential numbers
SELECT 
  name,
  salary,
  ROW_NUMBER() OVER (ORDER BY salary DESC) as rank
FROM employees;

-- RANK - Same rank for ties, gaps in sequence
SELECT
  name,
  score,
  RANK() OVER (ORDER BY score DESC) as rank
FROM scores;

-- DENSE_RANK - Same rank for ties, no gaps
SELECT
  name,
  score, 
  DENSE_RANK() OVER (ORDER BY score DESC) as dense_rank
FROM scores;

-- DETAILED_TAB:
-- Ranking within groups (PARTITION BY)
SELECT
  department,
  name,
  salary,
  ROW_NUMBER() OVER (
    PARTITION BY department 
    ORDER BY salary DESC
  ) as dept_rank,
  RANK() OVER (
    ORDER BY salary DESC
  ) as company_rank
FROM employees;

-- NTILE - Divide into buckets
SELECT
  name,
  salary,
  NTILE(4) OVER (ORDER BY salary) as quartile
FROM employees;
🟢 Essential - Window functions are PostgreSQL's superpower
💡 PARTITION BY is like GROUP BY for windows
📌 ROW_NUMBER always gives unique numbers
⚡ Window functions often faster than subqueries
🔗 Related: PERCENT_RANK, CUME_DIST for distributions
window-functionsranking

Aggregate Window Functions

Running totals, moving averages, and cumulative calculations

📄 Codesql
-- Running total
SELECT
  date,
  amount,
  SUM(amount) OVER (ORDER BY date) as running_total
FROM sales;

-- Moving average (3-row window)
SELECT
  date,
  price,
  AVG(price) OVER (
    ORDER BY date
    ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
  ) as moving_avg_3
FROM stock_prices;

-- Cumulative percentage
SELECT
  product,
  revenue,
  SUM(revenue) OVER (ORDER BY revenue DESC) as cumulative,
  100.0 * SUM(revenue) OVER (ORDER BY revenue DESC) / 
    SUM(revenue) OVER () as cumulative_pct
FROM product_sales;

-- DETAILED_TAB:
-- Complex window frames
SELECT
  date,
  sales,
  -- Previous row
  LAG(sales, 1) OVER (ORDER BY date) as prev_sales,
  -- Next row
  LEAD(sales, 1) OVER (ORDER BY date) as next_sales,
  -- First value in window
  FIRST_VALUE(sales) OVER (
    PARTITION BY EXTRACT(YEAR FROM date)
    ORDER BY date
  ) as year_first,
  -- Difference from previous
  sales - LAG(sales, 1) OVER (ORDER BY date) as growth
FROM daily_sales;
💡 ROWS BETWEEN defines the window frame
📌 LAG/LEAD access other rows without self-join
⚡ Running totals much faster than correlated subqueries
🟢 Essential for time-series analysis
🔗 Related: range-based windows with RANGE BETWEEN
window-functionsaggregates

CTEs & Advanced Queries

Common Table Expressions and recursive queries

Basic CTEs

Simplify complex queries with named subqueries

📄 Codesql
-- Simple CTE
WITH high_value_customers AS (
  SELECT customer_id, SUM(amount) as total
  FROM orders
  GROUP BY customer_id
  HAVING SUM(amount) > 1000
)
SELECT c.name, hvc.total
FROM customers c
JOIN high_value_customers hvc ON c.id = hvc.customer_id;

-- Multiple CTEs
WITH 
  active_users AS (
    SELECT * FROM users WHERE status = 'active'
  ),
  recent_orders AS (
    SELECT * FROM orders WHERE created_at > NOW() - INTERVAL '30 days'
  )
SELECT u.name, COUNT(o.id) as order_count
FROM active_users u
LEFT JOIN recent_orders o ON u.id = o.user_id
GROUP BY u.name;

-- DETAILED_TAB:
-- CTE with INSERT/UPDATE/DELETE
WITH deleted AS (
  DELETE FROM old_records
  WHERE created_at < NOW() - INTERVAL '1 year'
  RETURNING *
)
INSERT INTO archive_table
SELECT * FROM deleted;

-- Materialized CTE (PostgreSQL 12+)
WITH MATERIALIZED expensive_calculation AS (
  -- This runs once and results are stored
  SELECT complex_function(data) as result
  FROM large_table
)
SELECT * FROM expensive_calculation
WHERE result > 100;
🟢 Essential - CTEs make complex queries readable
💡 CTEs are like named temporary tables
📌 Can reference other CTEs defined earlier
⚡ MATERIALIZED forces CTE to run once
🔗 Related: Views for permanent named queries
ctewithsubqueries

Recursive CTEs

Process hierarchical and graph data

📄 Codesql
-- Organizational hierarchy
WITH RECURSIVE org_chart AS (
  -- Anchor: start with CEO
  SELECT id, name, manager_id, 0 as level
  FROM employees
  WHERE manager_id IS NULL
  
  UNION ALL
  
  -- Recursive: find subordinates
  SELECT e.id, e.name, e.manager_id, oc.level + 1
  FROM employees e
  JOIN org_chart oc ON e.manager_id = oc.id
)
SELECT * FROM org_chart
ORDER BY level, name;

-- DETAILED_TAB:
-- Generate series with recursive CTE
WITH RECURSIVE dates AS (
  SELECT DATE '2024-01-01' as date
  UNION ALL
  SELECT date + INTERVAL '1 day'
  FROM dates
  WHERE date < '2024-01-31'
)
SELECT date FROM dates;

-- Find all related records (graph traversal)
WITH RECURSIVE related AS (
  -- Start node
  SELECT id, parent_id, name, ARRAY[id] as path
  FROM categories
  WHERE id = 5
  
  UNION ALL
  
  -- Find connections
  SELECT c.id, c.parent_id, c.name, r.path || c.id
  FROM categories c
  JOIN related r ON c.parent_id = r.id
  WHERE NOT c.id = ANY(r.path)  -- Avoid cycles
)
SELECT * FROM related;
🔴 Advanced - Powerful but can be complex
💡 Always have a termination condition
⚠️ Watch for infinite loops - use path tracking
📌 Great for trees, graphs, and hierarchies
⚡ Consider ltree extension for hierarchies
recursivectehierarchy

Indexes & Performance

Optimize query performance with proper indexing

Index Types

Different index types for various use cases

📄 Codesql
-- B-tree index (default)
CREATE INDEX idx_email ON users(email);

-- Unique index
CREATE UNIQUE INDEX idx_username ON users(username);

-- Multicolumn index
CREATE INDEX idx_name ON users(last_name, first_name);

-- Partial index (filtered)
CREATE INDEX idx_active ON orders(user_id)
WHERE status = 'active';

-- Expression index
CREATE INDEX idx_lower_email ON users(LOWER(email));

-- DETAILED_TAB:
-- GIN index for full-text search
CREATE INDEX idx_search ON articles USING gin(to_tsvector('english', content));

-- GiST index for geometric data
CREATE INDEX idx_location ON stores USING gist(location);

-- BRIN index for large sorted tables
CREATE INDEX idx_created ON logs USING brin(created_at);

-- Hash index for equality checks only
CREATE INDEX idx_status ON orders USING hash(status);

-- Concurrent index creation (no table lock)
CREATE INDEX CONCURRENTLY idx_user_email ON users(email);
🟢 Essential - Indexes make queries fast
💡 B-tree good for most cases, GIN for arrays/JSON
⚡ Partial indexes save space and improve performance
⚠️ Too many indexes slow down writes
📌 Use CONCURRENTLY in production to avoid locks
indexesperformance

Query Analysis

Analyze and optimize query performance

📄 Codesql
-- Explain query plan
EXPLAIN SELECT * FROM users WHERE email = 'john@example.com';

-- Explain with execution stats
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 123;

-- Verbose explain with buffers
EXPLAIN (ANALYZE, BUFFERS, VERBOSE)
SELECT * FROM large_table WHERE status = 'active';

-- Check index usage
SELECT 
  schemaname,
  tablename,
  indexname,
  idx_scan,
  idx_tup_read,
  idx_tup_fetch
FROM pg_stat_user_indexes
ORDER BY idx_scan;

-- DETAILED_TAB:
-- Find missing indexes
SELECT
  schemaname,
  tablename,
  attname,
  n_distinct,
  correlation
FROM pg_stats
WHERE schemaname = 'public'
  AND n_distinct > 100
  AND correlation < 0.1
ORDER BY n_distinct DESC;

-- Query cache hit ratio
SELECT 
  sum(heap_blks_hit) / (sum(heap_blks_hit) + sum(heap_blks_read)) as cache_hit_ratio
FROM pg_statio_user_tables;

-- Slow query log (pg_stat_statements)
SELECT 
  query,
  mean_exec_time,
  calls,
  total_exec_time
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;
💡 EXPLAIN ANALYZE shows actual execution times
📌 Look for Seq Scan on large tables
⚡ Cache hit ratio should be > 95%
🟢 Essential for performance tuning
🔗 Related: pg_stat_statements extension
performanceanalysis

Optimization Techniques

Improve query performance with PostgreSQL-specific features

📄 Codesql
-- Vacuum and analyze
VACUUM ANALYZE users;            -- Clean up and update stats
VACUUM FULL users;               -- Full cleanup (locks table)

-- Table partitioning
CREATE TABLE orders_2024 PARTITION OF orders
FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');

-- Materialized views
CREATE MATERIALIZED VIEW monthly_sales AS
SELECT 
  DATE_TRUNC('month', created_at) as month,
  SUM(amount) as total
FROM orders
GROUP BY 1;

-- Refresh materialized view
REFRESH MATERIALIZED VIEW CONCURRENTLY monthly_sales;

-- DETAILED_TAB:
-- Parallel queries (PostgreSQL 9.6+)
SET max_parallel_workers_per_gather = 4;
SET parallel_setup_cost = 100;

-- Force index usage
SET enable_seqscan = OFF;  -- Force index (testing only!)

-- Work memory for sorts
SET work_mem = '256MB';    -- For current session

-- Prepared statements
PREPARE get_user (INT) AS
  SELECT * FROM users WHERE id = $1;
EXECUTE get_user(123);

-- Connection pooling config
ALTER SYSTEM SET max_connections = 200;
ALTER SYSTEM SET shared_buffers = '4GB';
SELECT pg_reload_conf();    -- Apply changes
💡 VACUUM reclaims dead tuple space
📌 Partitioning helps with large time-series data
⚡ Materialized views cache complex query results
⚠️ Don't disable seqscan in production
🔗 Related: pgpool, pgbouncer for connection pooling
optimizationperformance

Full-Text Search

Built-in full-text search capabilities

Text Search Basics

Set up and use PostgreSQL full-text search

📄 Codesql
-- Create text search column
ALTER TABLE articles ADD COLUMN search_vector tsvector;

-- Update search vector
UPDATE articles
SET search_vector = to_tsvector('english', 
  title || ' ' || content);

-- Create GIN index
CREATE INDEX idx_search ON articles USING gin(search_vector);

-- Search with tsquery
SELECT title, ts_rank(search_vector, query) as rank
FROM articles,
  to_tsquery('english', 'postgresql & performance') query
WHERE search_vector @@ query
ORDER BY rank DESC;

-- DETAILED_TAB:
-- Auto-update search vector with trigger
CREATE OR REPLACE FUNCTION update_search_vector()
RETURNS trigger AS $$
BEGIN
  NEW.search_vector := to_tsvector('english',
    COALESCE(NEW.title, '') || ' ' || 
    COALESCE(NEW.content, ''));
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trig_update_search
BEFORE INSERT OR UPDATE ON articles
FOR EACH ROW
EXECUTE FUNCTION update_search_vector();

-- Advanced search queries
SELECT * FROM articles
WHERE search_vector @@ to_tsquery('english',
  'postgresql & (index | performance) & !slow');

-- Highlight search results
SELECT ts_headline('english', content, query)
FROM articles, to_tsquery('english', 'database') query
WHERE search_vector @@ query;
🟢 Essential - Native FTS without external tools
💡 tsvector stores preprocessed searchable text
📌 Use appropriate language configuration
⚡ GIN index required for good performance
🔗 Related: pg_trgm for fuzzy text matching
searchfulltext

Transactions & Concurrency

ACID compliance and concurrent access control

Transaction Control

Manage transactions and isolation levels

📄 Codesql
-- Basic transaction
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;

-- Rollback on error
BEGIN;
UPDATE products SET stock = stock - 1 WHERE id = 123;
-- Error occurs here
ROLLBACK;

-- Savepoints
BEGIN;
UPDATE users SET credits = 100;
SAVEPOINT before_risky;
DELETE FROM logs;  -- Risky operation
ROLLBACK TO before_risky;  -- Undo just the DELETE
COMMIT;

-- DETAILED_TAB:
-- Isolation levels
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;    -- Default
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- Advisory locks
SELECT pg_advisory_lock(12345);        -- Get lock
-- Do work
SELECT pg_advisory_unlock(12345);      -- Release

-- Row-level locking
SELECT * FROM orders
WHERE id = 123
FOR UPDATE;                            -- Lock for update

SELECT * FROM inventory
WHERE product_id = 456  
FOR SHARE;                             -- Shared lock
🟢 Essential - Ensure data consistency
💡 Use transactions for related operations
⚠️ Long transactions can cause blocking
📌 Savepoints allow partial rollback
🔗 Related: pg_locks view for lock monitoring
transactionslocks