You need to generate the crosstab()
call dynamically.
But since SQL does not allow dynamic return types, you need a two-step workflow:
- Generate query
- Execute query
If you are unfamiliar with crosstab()
, read this first:
It's odd to generate the month from creation_date
, but not the year. To simplify, I use a combined column year_month
instead.
Query to generate the crosstab()
query:
SELECT format(
$f$SELECT * FROM crosstab(
$q$
SELECT to_char(date_trunc('month', creation_date), 'YYYY_Month') AS year_month
, marking
, COUNT(*) AS ct
FROM invoices
GROUP BY date_trunc('month', creation_date), marking
ORDER BY date_trunc('month', creation_date) -- optional
$q$
, $c$VALUES (%s)$c$
) AS ct(year_month text, %s);
$f$, string_agg(quote_literal(sub.marking), '), (')
, string_agg(quote_ident (sub.marking), ' int, ') || ' int'
)
FROM (SELECT DISTINCT marking FROM invoices ORDER BY 1) sub;
If the table invoices
is big with only few distinct values for marking (which seems likely) there are faster ways to get distinct values. See:
Generates a query of the form:
SELECT * FROM crosstab(
$q$
SELECT to_char(date_trunc('month', creation_date), 'YYYY_Month') AS year_month
, marking
, COUNT(*) AS ct
FROM invoices
GROUP BY date_trunc('month', creation_date), marking
ORDER BY date_trunc('month', creation_date) -- optional
$q$
, $c$VALUES ('Delivered'), ('Not Delivered'), ('Not Received')$c$
) AS ct(year_month text, "Delivered" int, "Not Delivered" int, "Not Received" int);
The simplified query does not need "extra columns. See:
Note the use date_trunc('month', creation_date)
in GROUP BY
and ORDER BY
. This produces a valid sort order, and faster, too. See:
Also note the use of dollar-quotes to avoid quoting hell. See:
Months without entries don't show up in the result, and no markings for an existing month show as NULL
. You can adapt either if need be. See:
Then execute the generated query.
db<>fiddle here (reusing
Edouard's fiddle, kudos!)
See:
In psql
In psql you can use \qexec
to immediately execute the generated query. See:
In Postgres 9.6 or later, you can also use the meta-command \crosstabview
instead of crosstab()
:
test=> SELECT to_char(date_trunc('month', creation_date), 'YYYY_Month') AS year_month
test-> , marking
test-> , COUNT(*) AS count
test-> FROM invoices
test-> GROUP BY date_trunc('month', creation_date), 2
test-> ORDER BY date_trunc('month', creation_date)\crosstabview
year_month | Not Received | Delivered | Not Delivered
----------------+--------------+-----------+---------------
2020_January | 1 | 1 | 1
2020_March | | 2 | 2
2021_January | 1 | 1 | 2
2021_February | 1 | |
2021_March | | 1 |
2021_August | 2 | 1 | 1
2022_August | | 2 |
2022_November | 1 | 2 | 3
2022_December | 2 | |
(9 rows)
Note that \crosstabview
- unlike crosstab()
- does not support "extra" columns. If you insist on separate year and month columns, you need crosstab()
.
See: