2

I have a scenario in a Postgres 9.3 database where I have to get the last 10 dates when books were sold. Consider below example:

   Store                Book
 ----------        ----------------------
 Id  Name          Id Name Sid Count Date
 1   ABC           1  XYZ  1   20    11/11/2015
 2   DEF           2  JHG  1   10    11/11/2015
                   3  UYH  1   10    15/11/2015
                   4  TRE  1   50    17/11/2015

There is currently no UNIQUE constraint on (name, sid, date) in table book, but we have a service in place that inserts only one count per day.

I have to get results based on store.id. When I pass the ID, the report should be generated with bookname, sold date, and the count of sold copies.

Desired output:

 BookName  11/11/2015 15/11/2015  17/11/2015
 XYZ       20         --          --
 JHG       10         --          --
 UYH       --         10          --
 TRE       --         --          50
Erwin Brandstetter
  • 605,456
  • 145
  • 1,078
  • 1,228
manthan davda
  • 319
  • 1
  • 3
  • 13

1 Answers1

2

This looks unsuspicious, but it's a hell of a question.

Assumptions

crosstab() queries

To get top performance and short query strings (especially if you run this query often) I suggest the additional module tablefunc providing various crosstab() functions. Basic instructions:

Basic queries

You need to get these right first.

The last 10 days:

SELECT DISTINCT date
FROM   book
WHERE  sid = 1
ORDER  BY date DESC
LIMIT  10;

Numbers for last 10 days using the window function dense_rank():

SELECT *
FROM  (
   SELECT name
        , dense_rank() OVER (ORDER BY date DESC) AS date_rnk
        , count
   FROM   book
   WHERE  sid = 1
   ) sub
WHERE  date_rnk < 11
ORDER  BY name, date_rnk DESC;

(Not including actual dates in this query.)

Column names for output columns (for full solution):

SELECT 'bookname, "' || string_agg(to_char(date, 'DD/MM/YYYY'), '", "' ORDER BY date) || '"'
FROM  (
   SELECT DISTINCT date
   FROM   book
   WHERE  sid = 1
   ORDER  BY date DESC
   LIMIT  10
   ) sub;

Simple result with static column names

This may be good enough for you - but we don't see actual dates in the result:

SELECT * FROM crosstab(
  'SELECT *
   FROM  (
      SELECT name
           , dense_rank() OVER (ORDER BY date DESC) AS date_rnk
           , count
      FROM   book
      WHERE  sid = 1
      ) sub
   WHERE  date_rnk < 11
   ORDER  BY name, date_rnk DESC'
, 'SELECT generate_series(10, 1, -1)'
 ) AS (bookname text
     , date1 int, date2 int, date3 int, date4 int, date5 int
     , date6 int, date7 int, date8 int, date9 int, date10 int);

For repeated use I suggest you create this (very fast) generic C function for 10 integer columns once, to simplify things a bit:

CREATE OR REPLACE FUNCTION crosstab_int10(text, text)
  RETURNS TABLE (bookname text
               , date1 int, date2 int, date3 int, date4 int, date5 int
               , date6 int, date7 int, date8 int, date9 int, date10 int)
  LANGUAGE C STABLE STRICT AS
'$libdir/tablefunc','crosstab_hash';

Details in this related answer:

Then your call becomes:

SELECT * FROM crosstab(
  'SELECT *
   FROM  (
      SELECT name
           , dense_rank() OVER (ORDER BY date DESC) AS date_rnk
           , count
      FROM   book
      WHERE  sid = 1
      ) sub
   WHERE  date_rnk < 11
   ORDER  BY name, date_rnk DESC'
, 'SELECT generate_series(10, 1, -1)'
 );  -- no column definition list required!

Full solution with dynamic column names

Your actual question is more complicated, you also want dynamic column names.
For a given table, the resulting query could look like this then:

SELECT * FROM crosstab_int10(
  'SELECT *
   FROM  (
      SELECT name
           , dense_rank() OVER (ORDER BY date DESC) AS date_rnk
           , count
      FROM   book
      WHERE  sid = 1
      ) sub
   WHERE  date_rnk < 11
   ORDER  BY name, date_rnk DESC'
, 'SELECT generate_series(10, 1, -1)'
   ) AS t(bookname
        , "04/11/2015", "05/11/2015", "06/11/2015", "07/11/2015", "08/11/2015"
        , "09/11/2015", "10/11/2015", "11/11/2015", "15/11/2015", "17/11/2015");

The difficulty is to distill dynamic column names. Either assemble the query string by hand, or (much rather) let this function do it for you:

CREATE OR REPLACE FUNCTION f_generate_date10_sql(_sid int = 1) 
  RETURNS text
  LANGUAGE sql AS
$func$
SELECT format(
 $$SELECT * FROM crosstab_int10(
  'SELECT *
   FROM  (
      SELECT name
           , dense_rank() OVER (ORDER BY date DESC) AS date_rnk
           , count
      FROM   book
      WHERE  sid = %1$s
      ) sub
   WHERE  date_rnk < 11
   ORDER  BY name, date_rnk DESC'
, 'SELECT generate_series(10, 1, -1)'
   ) AS ct(bookname, "$$
|| string_agg(to_char(date, 'DD/MM/YYYY'), '", "' ORDER BY date) || '")'
 , _sid)
FROM  (
   SELECT DISTINCT date
   FROM   book
   WHERE  sid = 1
   ORDER  BY date DESC
   LIMIT  10
   ) sub
$func$;

Call:

SELECT f_generate_date10_sql(1);

This generates the desired query, which you execute in turn.

db<>fiddle here

Erwin Brandstetter
  • 605,456
  • 145
  • 1,078
  • 1,228
  • Woah, You are genuis.. This thing has helped me lot.. I have got two more changes in requirement.. 1) Instead of last 10 dates, user will pass the number so it would be now last N dates. 2) All the books should be returned for that store even it doesn't have any count..User can pass multiple stored id and result will be returned..My problem are increasing day by day :( – manthan davda Oct 04 '15 at 11:31
  • @manthandavda: Comments are not the place. Start a new question for each new question. Make it one question per question, provide the necessary context and show what you tried. You can always link to this one for context. – Erwin Brandstetter Oct 04 '15 at 12:26
  • http://stackoverflow.com/questions/32933910/get-dates-for-last-n-records-by-converting-rows-to-column-postgressql – manthan davda Oct 04 '15 at 13:17
  • One last question how can i execute query returns from f_generate_date10_sql?? – manthan davda Oct 04 '15 at 14:57
  • @manthandavda: The same way you execute the first query. That means two round trips to the DB server. – Erwin Brandstetter Oct 04 '15 at 18:31
  • Gezz... The ROI on this answer does not do it justice. – Arvo Bowen May 22 '20 at 18:52
  • @ArvoBowen: With the ROI improving dramatically, I invested more! Seriously, your comment brought this old answer to my attention. And there was room for improvement. ROI is not nearly as motivating as the compulsion to get the code right. :) – Erwin Brandstetter May 22 '20 at 23:19