Question

My actual table structures are much more complex but following are two simplified table definitions:

Table invoice

CREATE TABLE invoice (
  id integer NOT NULL,
  create_datetime timestamp with time zone NOT NULL,
  total numeric(22,10) NOT NULL
);

id   create_datetime   total    
----------------------------
100  2014-05-08        1000

Table payment_invoice

CREATE TABLE payment_invoice (
  invoice_id integer,
  amount numeric(22,10)
);

invoice_id  amount
-------------------
100         100
100         200
100         150

I want to select the data by joining above 2 tables and selected data should look like:-

month      total_invoice_count  outstanding_balance
05/2014    1                    550

The query I am using:

select
to_char(date_trunc('month', i.create_datetime), 'MM/YYYY') as month,
count(i.id) as total_invoice_count,
(sum(i.total) - sum(pi.amount)) as outstanding_balance
from invoice i
join payment_invoice pi on i.id=pi.invoice_id
group by date_trunc('month', i.create_datetime)
order by date_trunc('month', i.create_datetime);

Above query is giving me incorrect results as sum(i.total) - sum(pi.amount) returns (1000 + 1000 + 1000) - (100 + 200 + 150) = 2550.
I want it to return (1000) - (100 + 200 + 150) = 550

And I cannot change it to i.total - sum(pi.amount), because then I am forced to add i.total column to group by clause and that I don't want to do.

Was it helpful?

Solution

You need a single row per invoice, so aggregate payment_invoice first - best before you join.
When the whole table is selected, it's typically fastest to aggregate first and join later:

SELECT to_char(date_trunc('month', i.create_datetime), 'MM/YYYY') AS month
     , count(*)                                   AS total_invoice_count
     , (sum(i.total) - COALESCE(sum(pi.paid), 0)) AS outstanding_balance
FROM   invoice i
LEFT   JOIN  (
    SELECT invoice_id AS id, sum(amount) AS paid
    FROM   payment_invoice pi
    GROUP  BY 1
    ) pi USING (id)
GROUP  BY date_trunc('month', i.create_datetime)
ORDER  BY date_trunc('month', i.create_datetime);

LEFT JOIN is essential here. You do not want to loose invoices that have no corresponding rows in payment_invoice (yet), which would happen with a plain JOIN.

Accordingly, use COALESCE() for the sum of payments, which might be NULL.

SQL Fiddle with improved test case.

OTHER TIPS

Do the aggregation in two steps. First aggregate to a single line per invoice, then to a single line per month:

select
  to_char(date_trunc('month', t.create_datetime), 'MM/YYYY') as month,
  count(*) as total_invoice_count,
  (sum(t.total) - sum(t.amount)) as outstanding_balance
from (
    select i.create_datetime, i.total, sum(pi.amount) amount
    from invoice i
    join payment_invoice pi on i.id=pi.invoice_id
    group by i.id, i.total
) t
group by date_trunc('month', t.create_datetime)
order by date_trunc('month', t.create_datetime);

See sqlFiddle

SELECT TO_CHAR(invoice.create_datetime, 'MM/YYYY') as month,
       COUNT(invoice.create_datetime) as total_invoice_count,
       invoice.total - payments.sum_amount as outstanding_balance
FROM invoice
JOIN 
(
    SELECT invoice_id, SUM(amount) AS sum_amount
    FROM payment_invoice
    GROUP BY invoice_id
) payments
ON invoice.id = payments.invoice_id
GROUP BY TO_CHAR(invoice.create_datetime, 'MM/YYYY'), 
         invoice.total - payments.sum_amount
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top