-->

How to get number of invoices for last 12 weeks in

2019-08-04 09:42发布

问题:

Invoice database contains invoice dates:

create table dok (
  dokumnr serial primary key,
  invoicedate date not null
);

Dashboard requires comma separated list containing number of invoices for last 12 weeks, e.q

4,8,0,6,7,6,0,6,0,4,5,6

List contains always 12 elements. If there are no invoices for some 7 day interval, 0 should appear. Every element should contain number of invoices for 7 days.

Query should find maximum date before current date:

select max(invoicedate) as last_date from dok;

And after that probably use count(*) and string_agg() to create list.

Last (12th) element should contain number of invoices for

  last_date .. last_date-interval'6days'

11 element (one before last) should contain number of invoices for days

  last_date-interval'7days' .. last_date-interval'14days'

etc.

How to write this query in Postgres 9.1+ ? This is ASP.NET MVC3 C# application and some parts of query can also done in C# code if this helps.

I ended with

with list as (

SELECT count(d.invoicedate) as cnt
   FROM  (
      SELECT max(invoicedate) AS last_date
      FROM   dok
      WHERE   invoicedate< current_date
      ) l
   CROSS  JOIN generate_series(0, 11*7, 7) AS g(days)
   LEFT   JOIN dok d ON d.invoicedate>  l.last_date - g.days - 7
                    AND d.invoicedate<= l.last_date - g.days
   GROUP  BY g.days
   ORDER  BY g.days desc
)

SELECT string_agg( cnt::text,',')
from list

回答1:

CROSS JOIN the latest date to generate_series(), followed by a LEFT JOIN to the main table.

SELECT ARRAY(
   SELECT count(d.invoicedate) AS ct
   FROM  (
      SELECT max(invoicedate) AS last_date
      FROM   dok
      WHERE  invoicedate < current_date  -- "maximum date before current date"
      ) l
   CROSS  JOIN generate_series(0, 11*7, 7) AS g(days)
   LEFT   JOIN dok d ON d.invoicedate >  l.last_date - g.days - 7
                    AND d.invoicedate <= l.last_date - g.days
   GROUP  BY g.days
   ORDER  BY g.days
   );

Assuming there is at least one valid entry in the table,
this returns an array of bigint (bigint[]) with the latest week first.

current_date depends on the timezone setting of your session.

If you need the result to be a comma-separated string you could use another query layer with string_agg() instead. Or you feed the above to array_to_string():

SELECT array_to_string(ARRAY(SELECT ...), ',');

Your query audited:

It's an implementation detail, but it's documented:

The aggregate functions array_agg, json_agg, jsonb_agg, json_object_agg, jsonb_object_agg, string_agg, and xmlagg, as well as similar user-defined aggregate functions, produce meaningfully different result values depending on the order of the input values. This ordering is unspecified by default, but can be controlled by writing an ORDER BY clause within the aggregate call, as shown in Section 4.2.7. Alternatively, supplying the input values from a sorted subquery will usually work. For example:

SELECT xmlagg(x) FROM (SELECT x FROM test ORDER BY y DESC) AS tab;

Beware that this approach can fail if the outer query level contains additional processing, such as a join, because that might cause the subquery's output to be reordered before the aggregate is computed.

Bold emphasis mine.
To stay standard compliant, you could write:

WITH list AS (
   SELECT g.days, count(d.invoicedate)::text AS cnt
   FROM  (
      SELECT max(invoicedate) AS last_date
      FROM   dok
      WHERE  invoicedate < current_date
      ) l
   CROSS  JOIN generate_series(0, 11*7, 7) AS g(days)
   LEFT   JOIN dok d ON d.invoicedate >  l.last_date - g.days - 7
                    AND d.invoicedate <= l.last_date - g.days
   GROUP  BY 1
   )
SELECT string_agg(cnt, ',' ORDER BY days DESC)
FROM   list;

But this is a bit slower. Also, the CTE is not technically necessary and also a bit slower than a subquery.
SELECT array_to_string(ARRAY( SELECT ...), ',') like I proposed is fastest because the array constructor is faster for a single result than the aggregate function string_agg().