$ lexprog.com

// notes from an old coder -- php, databases, and the occasional rant

[February 16, 2025] PostgreSQL

PostgreSQL Crosstab: Pivot Queries

PostgreSQL Crosstab: Pivot Queries

────────────────────────────────────────────────────────

PostgreSQL Crosstab: Pivot Queries

Tip: Enable tablefunc

CREATE EXTENSION tablefunc;

Gotcha: Crosstab Function

SELECT * FROM crosstab(
    'SELECT category, month, revenue FROM sales ORDER BY 1, 2',
    'SELECT DISTINCT month FROM sales ORDER BY 1'
) AS ct (category text, jan int, feb int, mar int);

Tip: Pivot Rows to Columns

Transforms row data into a column-based report format.

Gotcha: Column Definition Required

You must define all output columns explicitly. Dynamic columns aren't supported.

Tip: Alternative with FILTER

SELECT category,
    sum(revenue) FILTER (WHERE month = 'jan') as jan,
    sum(revenue) FILTER (WHERE month = 'feb') as feb
FROM sales GROUP BY category;

Often simpler than crosstab.

Gotcha: NULL Handling

Missing values become NULL. Use COALESCE to replace with 0.

Tip: EXPLAIN (ANALYZE, BUFFERS) Is Your Best Friend

For query debugging, always use EXPLAIN (ANALYZE, BUFFERS) instead of plain EXPLAIN. The BUFFERS option shows hit/miss rates for every node, revealing whether your indexes are actually in memory.

Tip: Partial Indexes Are Underutilized

CREATE INDEX ON orders (status) WHERE status = 'pending' creates a tiny index that covers only the rows your query needs. It's faster to scan and cheaper to maintain than a full-column index.

Gotcha: NULL Sorting Is Non-Obvious

By default, NULLs sort AFTER non-null values in ascending order. ORDER BY col DESC puts NULLs FIRST. Use NULLS LAST or NULLS FIRST to be explicit.

Senior Insight

The tablefunc extension's crosstab() function turns rows into columns — essentially a PIVOT operation. I've used it for reporting dashboards that need month-over-month comparisons in a single result set. The function is powerful but finicky: it requires exactly three columns (row_name, category, value) and the category values must be specified in a second query. I write a wrapper function for each crosstab query to make it reusable.

Source: pganalyze Blog (https://pganalyze.com/blog), PostgreSQL Docs (https://www.postgresql.org/docs/current/), Crunchy Data Blog (https://www.crunchydata.com/blog)

────────────────────────────────────────────────────────
<-- back to posts