Examples
Five PostgreSQL recipes that come up in real apps: upsert, window queries, JSON columns, materialised views, and bulk loading. Each is paste-ready.
Five Postgres recipes
EXAMPLE
-- 1) UPSERT via ON CONFLICT
CREATE TABLE customers (
id bigserial PRIMARY KEY,
email text NOT NULL UNIQUE,
name text NOT NULL,
updated_at timestamptz NOT NULL DEFAULT now()
);
INSERT INTO customers (email, name) VALUES ('alice@example.com', 'Alice')
ON CONFLICT (email) DO UPDATE
SET name = EXCLUDED.name,
updated_at = now()
RETURNING id, name;
-- 2) Window queries — top 3 orders per customer
SELECT *
FROM (
SELECT id, customer_id, total_cents,
ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY total_cents DESC) AS rk
FROM orders
) ranked
WHERE rk <= 3;
-- Running total per customer
SELECT id, customer_id, created_at, total_cents,
SUM(total_cents) OVER (PARTITION BY customer_id ORDER BY created_at) AS running
FROM orders;
-- 7-day moving average of revenue
SELECT day, revenue,
AVG(revenue) OVER (ORDER BY day ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS ma_7
FROM daily_revenue;
-- 3) JSONB column queries + GIN index
CREATE TABLE events (
id bigserial PRIMARY KEY,
type text NOT NULL,
payload jsonb NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX ix_events_payload_gin ON events USING gin (payload jsonb_path_ops);
INSERT INTO events (type, payload) VALUES
('order.paid', '{"order_id": "o1", "amount": 4995}'),
('order.shipped', '{"order_id": "o1", "tracking": "AU-9F3"}');
-- Lookup by path
SELECT id, type, payload->>'order_id' AS oid
FROM events
WHERE payload @> '{"order_id":"o1"}';
-- Modify JSONB
UPDATE events SET payload = payload || '{"version": 2}'
WHERE id = 1;
UPDATE events SET payload = jsonb_set(payload, '{amount}', '6000')
WHERE type = 'order.paid';
-- 4) Materialised view — cached aggregate
CREATE MATERIALIZED VIEW customer_lifetime_value AS
SELECT customer_id,
SUM(total_cents) AS total_cents,
COUNT(*) AS order_count,
MAX(created_at) AS last_order_at
FROM orders
WHERE status IN ('paid', 'shipped')
GROUP BY customer_id;
CREATE UNIQUE INDEX ix_clv_customer ON customer_lifetime_value (customer_id);
-- Refresh (CONCURRENTLY needs a unique index)
REFRESH MATERIALIZED VIEW CONCURRENTLY customer_lifetime_value;
-- 5) Bulk load with COPY (much faster than INSERT-per-row)
COPY orders (customer_id, total_cents, status, created_at)
FROM '/tmp/orders.csv' WITH (FORMAT csv, HEADER true);
-- Or from stdin via psql:
-- \copy orders FROM 'orders.csv' WITH CSV HEADER
-- ===== Patterns to internalise =====
-- - ON CONFLICT for upserts; never SELECT-then-INSERT for the same purpose
-- - Window functions replace many self-joins
-- - GIN index on jsonb_path_ops for @> queries; smaller than the default GIN
-- - Materialised view + CONCURRENTLY refresh for cached aggregates
-- - COPY for bulk; orders of magnitude faster than per-row INSERT
-- ===== Pitfalls =====
-- - ON CONFLICT without a constraint to target -> error
-- - REFRESH MATERIALIZED VIEW (without CONCURRENTLY) locks reads
-- - Forgetting to ANALYZE after bulk load -> bad plans for a while
-- - JSONB queries without an index -> sequential scans
-- - Window function results ordered randomly when ORDER BY is missing
Why it matters
`COPY ... FROM` is the single most under-used Postgres feature for bulk loading. Orders of magnitude faster than `INSERT` per row, parses CSV/binary natively, and ANALYZEs cleanly afterwards. Reach for it the moment you have more than a few thousand rows to load — the difference between minutes and hours.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Discussion
Loading…