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: