PostgreSQL Is More Than a Database
Most developers use PostgreSQL as a simple data store - INSERT, SELECT, UPDATE, DELETE. But PostgreSQL has advanced features that can replace hundreds of lines of application code with a single query, improve performance by orders of magnitude, and simplify your architecture.
Here's what you're probably not using - but should be.
Common Table Expressions (CTEs)
CTEs make complex queries readable and maintainable. Think of them as temporary named result sets within a query:
WITH monthly_revenue AS (
SELECT
DATE_TRUNC('month', created_at) AS month,
SUM(amount) AS revenue,
COUNT(DISTINCT customer_id) AS unique_customers
FROM orders
WHERE status = 'completed'
GROUP BY DATE_TRUNC('month', created_at)
),
growth AS (
SELECT
month,
revenue,
unique_customers,
LAG(revenue) OVER (ORDER BY month) AS prev_month_revenue,
ROUND(
((revenue - LAG(revenue) OVER (ORDER BY month))
/ LAG(revenue) OVER (ORDER BY month)) * 100, 1
) AS growth_pct
FROM monthly_revenue
)
SELECT * FROM growth ORDER BY month DESC LIMIT 12;
This single query gives you monthly revenue, customer count, and month-over-month growth - no application code needed.
Window Functions
Window functions analyze data across rows without collapsing them. They're incredibly powerful for analytics:
SELECT
name,
department,
salary,
AVG(salary) OVER (PARTITION BY department) AS dept_avg,
salary - AVG(salary) OVER (PARTITION BY department) AS diff_from_avg,
RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dept_rank,
NTILE(4) OVER (ORDER BY salary DESC) AS salary_quartile
FROM employees;
Useful Window Functions
| Function | Purpose | Example Use |
|---|---|---|
| ROW_NUMBER() | Sequential numbering | Pagination, deduplication |
| RANK() / DENSE_RANK() | Ranking with/without gaps | Leaderboards, top-N queries |
| LAG() / LEAD() | Access previous/next row | Trend analysis, change detection |
| SUM() OVER | Running totals | Cumulative metrics |
| AVG() OVER | Moving averages | Smoothed time-series data |
| NTILE(n) | Distribute into n buckets | Percentile analysis |
JSONB: Structured Flexibility
PostgreSQL's JSONB type gives you document database flexibility with relational database reliability:
-- Store flexible product attributes
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
price DECIMAL NOT NULL,
attributes JSONB DEFAULT '{}'
);
-- Query nested JSON efficiently
SELECT name, price
FROM products
WHERE attributes->>'category' = 'electronics'
AND (attributes->'specs'->>'memory_gb')::int > 8
AND attributes->'tags' ? 'premium';
-- Create a GIN index for fast JSON queries
CREATE INDEX idx_product_attrs ON products USING GIN (attributes);
When to Use JSONB vs. Columns
| Use JSONB When | Use Columns When |
|---|---|
| Schema varies between records | Schema is consistent |
| You're storing nested/complex data | Data is flat and relational |
| Schema evolves frequently | Schema is stable |
| You need flexible filtering | You need joins and foreign keys |
Full-Text Search
Skip Elasticsearch for simple search needs - PostgreSQL handles it natively:
-- Add a search vector column
ALTER TABLE articles ADD COLUMN search_vector tsvector;
-- Update it with weighted content
UPDATE articles SET search_vector =
setweight(to_tsvector('english', title), 'A') ||
setweight(to_tsvector('english', COALESCE(summary, '')), 'B') ||
setweight(to_tsvector('english', body), 'C');
-- Create a GIN index
CREATE INDEX idx_search ON articles USING GIN (search_vector);
-- Search with ranking
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
LIMIT 20;
Performance Optimization
EXPLAIN ANALYZE: Your Best Friend
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM orders
WHERE customer_id = 123
AND created_at > '2024-01-01'
ORDER BY created_at DESC
LIMIT 10;
Read the output. Look for sequential scans on large tables (add an index), nested loops on large datasets (consider a hash join), and high buffer usage (query is reading too much data).
Index Types and When to Use Them
| Index Type | Best For | Example |
|---|---|---|
| B-tree (default) | Equality, range, sorting | WHERE price > 100 ORDER BY created_at |
| GIN | Arrays, JSONB, full-text | WHERE tags @> '{premium}' |
| GiST | Geometric, range types | WHERE location <-> point(0,0) < 100 |
| BRIN | Large, naturally ordered tables | WHERE created_at BETWEEN ... (on time-series) |
| Hash | Equality only | WHERE status = 'active' (rare edge case) |
Table Partitioning
For tables with millions of rows, partitioning dramatically improves query performance:
CREATE TABLE events (
id BIGSERIAL,
event_type TEXT,
payload JSONB,
created_at TIMESTAMPTZ NOT NULL
) PARTITION BY RANGE (created_at);
CREATE TABLE events_2024_q1 PARTITION OF events
FOR VALUES FROM ('2024-01-01') TO ('2024-04-01');
CREATE TABLE events_2024_q2 PARTITION OF events
FOR VALUES FROM ('2024-04-01') TO ('2024-07-01');
Conclusion
PostgreSQL's advanced features can replace significant amounts of application code with efficient, well-optimized database operations. Master CTEs for complex queries, window functions for analytics, JSONB for flexible data, and proper indexing for performance. Your application will be simpler, faster, and more maintainable.
Building a data-intensive application? Our custom software development team designs database architectures that scale. Let's discuss your project.


