Search code examples
sqlpostgresqlrollup

How can I translate this unscalable query into a window function for dynamic columns?


I have a query that I use to generate the total order amount for customers and group them into columns by month alongside another column that represents total order amount.

Here's the schema:

temp=# \d+ customers;
                          Table "pg_temp_2.customers"
   Column   |            Type             | Modifiers | Storage  | Description 
------------+-----------------------------+-----------+----------+-------------
 id         | integer                     | not null  | plain    | 
 created_at | timestamp without time zone |           | plain    | 
 name       | text                        |           | extended | 
Indexes:
    "customers_pkey" PRIMARY KEY, btree (id)
Referenced by:
    TABLE "orders" CONSTRAINT "orders_customer_id_fkey" FOREIGN KEY (customer_id) REFERENCES customers(id)
Has OIDs: no


                             Table "pg_temp_2.orders"
   Column    |            Type             |   Modifiers   | Storage | Description 
-------------+-----------------------------+---------------+---------+-------------
 id          | integer                     | not null      | plain   | 
 created_at  | timestamp without time zone | default now() | plain   | 
 customer_id | integer                     |               | plain   | 
 amount      | integer                     |               | plain   | 
Indexes:
    "orders_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
    "orders_customer_id_fkey" FOREIGN KEY (customer_id) REFERENCES customers(id)
Has OIDs: no

For convenience, I've added the create table statements:

create temporary table customers ( id integer primary key, created_at timestamp without time zone, name text); 

create temporary table orders ( id integer primary key, created_at timestamp without time zone, customer_id integer references customers(id));

Here's the query I'm using:

SELECT
  c.name,
  sum(o.amount),
  CAST(SUM(
   CASE
        WHEN date_trunc('month', o.created_at) BETWEEN '2012-10-01' AND ('2012-11-01'::date - '1 day'::interval)
        THEN o.amount
        ELSE 0
   END
   ) / 100.0 AS MONEY) october2012,
   CAST(SUM(
   CASE
        WHEN date_trunc('month', o.created_at) BETWEEN '2012-11-01' AND ('2012-12-01'::date - '1 day'::interval)
        THEN o.amount
        ELSE 0
   END
   ) / 100.0 AS MONEY) as november2012
FROM orders o
INNER JOIN customers c ON o.customer_id = c.id
WHERE o.created_at >= '01 October 2012'
  AND o.created_At < '01 December 2012'
GROUP BY
 c.name
ORDER BY
  october2012 desc;

How can I get rid of that ugly case statement? There MUST be a more elegant way that rolls up these queries over a certain time slice. I tried to use window functions, but I've failed miserably. Any assistance would be appreciated!

I'm using postgresql 9.1


Solution

  • This is not simpler but is more scalable:

    select *
    from crosstab($$
        with orders as (
            select
                customer_id,
                date_trunc('month', created_at) created_at,
                cast(amount / 100.0 as money) amount
            from orders
            where
                created_at >= '2012-10-01'
                and created_at < '2012-12-01'
        ), months as (
            select
                c.name,
                to_char(o.created_at, 'YYYY-MM') created_at,
                sum(o.amount) amount
            from
                orders o
                inner join
                customers c on c.id = o.customer_id
            group by 1, 2
        )
        select name, created_at, amount
        from months
        union all
        select name, 'Total', sum(amount)
        from months
        group by 1, 2
        order by 1, 2
        $$, $$
        select distinct to_char(created_at, 'YYYY-MM')
        from orders
        where
            created_at >= '2012-10-01'
            and created_at < '2012-12-01'
        union select 'Total'
        order by 1
        $$
    ) as (name text, "2012-10" money, "2012-11" money, "Total" money)