By schone

2010-06-09 01:13:03 8 Comments

Does any one know how to create crosstab queries in PostgreSQL?
For example I have the following table:

Section    Status    Count
A          Active    1
A          Inactive  2
B          Active    4
B          Inactive  5

I would like the query to return the following crosstab:

Section    Active    Inactive
A          1         2
B          4         5

Is this possible?


@Erwin Brandstetter 2012-08-01 02:48:06

Install the additional module tablefunc once per database, which provides the function crosstab(). Since Postgres 9.1 you can use CREATE EXTENSION for that:


Improved test case

   section   text
 , status    text
 , ct        integer  -- "count" is a reserved word in standard SQL

  ('A', 'Active', 1), ('A', 'Inactive', 2)
, ('B', 'Active', 4), ('B', 'Inactive', 5)
                    , ('C', 'Inactive', 7);  -- ('C', 'Active') is missing

Simple form - not fit for missing attributes

crosstab(text) with 1 input parameter:

FROM   crosstab(
   'SELECT section, status, ct
    FROM   tbl
    ORDER  BY 1,2'  -- needs to be "ORDER BY 1,2" here
   ) AS ct ("Section" text, "Active" int, "Inactive" int);


 Section | Active | Inactive
 A       |      1 |        2
 B       |      4 |        5
 C       |      7 |           -- !!
  • No need for casting and renaming.
  • Note the incorrect result for C: the value 7 is filled in for the first column. Sometimes, this behavior is desirable, but not for this use case.
  • The simple form is also limited to exactly three columns in the provided input query: row_name, category, value. There is no room for extra columns like in the 2-parameter alternative below.

Safe form

crosstab(text, text) with 2 input parameters:

FROM   crosstab(
   'SELECT section, status, ct
    FROM   tbl
    ORDER  BY 1,2'  -- could also just be "ORDER BY 1" here

  , $$VALUES ('Active'::text), ('Inactive')$$
   ) AS ct ("Section" text, "Active" int, "Inactive" int);


 Section | Active | Inactive
 A       |      1 |        2
 B       |      4 |        5
 C       |        |        7  -- !!
  • Note the correct result for C.

  • The second parameter can be any query that returns one row per attribute matching the order of the column definition at the end. Often you will want to query distinct attributes from the underlying table like this:

    'SELECT DISTINCT attribute FROM tbl ORDER BY 1'

    That's in the manual.

    Since you have to spell out all columns in a column definition list anyway (except for pre-defined crosstabN() variants), it is typically more efficient to provide a short list in a VALUES expression like demonstrated:

    $$VALUES ('Active'::text), ('Inactive')$$)

    Or (not in the manual):

    $$SELECT unnest('{Active,Inactive}'::text[])$$  -- short syntax for long lists
  • I used dollar quoting to make quoting easier.

  • You can even output columns with different data types with crosstab(text, text) - as long as the text representation of the value column is valid input for the target type. This way you might have attributes of different kind and output text, date, numeric etc. for respective attributes. There is a code example at the end of the chapter crosstab(text, text) in the manual.

db<>fiddle here

Advanced examples

\crosstabview in psql

Postgres 9.6 added this meta-command to its default interactive terminal psql. You can run the query you would use as first crosstab() parameter and feed it to \crosstabview (immediately or in the next step). Like:

db=> SELECT section, status, ct FROM tbl \crosstabview

Similar result as above, but it's a representation feature on the client side exclusively. Input rows are treated slightly differently, hence ORDER BY is not required. Details for \crosstabview in the manual. There are more code examples at the bottom of that page.

Related answer on dba.SE by Daniel Vérité (the author of the psql feature):

The previously accepted answer is outdated.

  • The variant of the function crosstab(text, integer) is outdated. The second integer parameter is ignored. I quote the current manual:

    crosstab(text sql, int N) ...

    Obsolete version of crosstab(text). The parameter N is now ignored, since the number of value columns is always determined by the calling query

  • Needless casting and renaming.

  • It fails if a row does not have all attributes. See safe variant with two input parameters above to handle missing attributes properly.

  • ORDER BY is required in the one-parameter form of crosstab(). The manual:

    In practice the SQL query should always specify ORDER BY 1,2 to ensure that the input rows are properly ordered

@Jeremiah Peschka 2012-08-02 04:06:24

Thanks for providing an updated answer. I wasn't aware of the changes to the crosstab function.

@ChristopheD 2012-08-09 11:15:55

+1, good writeup, thanks for noticing In practice the SQL query should always specify ORDER BY 1,2 to ensure that the input rows are properly ordered

@Marco Fantasia 2014-03-05 11:47:53

I've some problems using $$VALUES .. $$. I've used instead 'VALUES (''<attr>'':: <type>), .. '

@Ashish 2015-10-22 09:29:54

Can we specify parameter binding in crosstab query ? I am getting this error => could not determine data type of parameter $2

@Ashish 2015-10-22 10:27:52

Is it possible to set default value for column in crosstab query ?

@Erwin Brandstetter 2015-10-23 04:48:50

@Ashish: Please start a new question. Comments are not the place. You can always link to this one for context.

@araqnid 2010-06-09 18:31:53

SELECT section,
       SUM(CASE status WHEN 'Active' THEN count ELSE 0 END) AS active, --here you pivot each status value as a separate column explicitly
       SUM(CASE status WHEN 'Inactive' THEN count ELSE 0 END) AS inactive --here you pivot each status  value as a separate column explicitly

GROUP BY section

@John Powell 2012-08-01 09:58:18

Can someone explain what the crosstab function in the tablefunc module adds to this answer, which both does the job at hand, and to my mind is easier to understand?

@Erwin Brandstetter 2012-08-01 13:41:04

@JohnBarça: A simple case like this can easily be solved with CASE statements. However, this gets unwieldy very quickly with more attributes and / or other data types than just integers. As an aside: this form uses the aggregate function sum(), it would be better to use min() or max() and no ELSE which works for text also. But this has subtly different effects than corosstab(), which only uses the "first" value per attribute. Doesn't matter as long as there can only be one. Finally, performance is relevant, too. crosstab() is written in C and optimized for the task.

@John Powell 2012-08-07 10:34:47

@ErwinBrandstetter, thanks for the explanation.

@Audrey 2014-11-12 12:05:47

This does not work for me, for postgresql. I get the error ERROR: 42803: aggregate function calls may not be nested

@user533832 2014-12-06 14:33:23

@Audrey you aren't running the same SQL then?

@Daniel L. VanDenBosch 2017-06-29 15:12:16

Consider adding explaination vs just a block of code

@FatihAkici 2018-10-05 19:37:27

In my postgresql for some reason tablefunc and crosstab are not defined, and I am not permitted to define them. This intuitive solution worked for me, so thums up!

@Vladimir Stazhilov 2018-11-28 12:10:23

that's a good option, but what if you have 300 categories?

@Lekshmi Kurup 2018-04-12 09:23:35

Crosstab function is available under the tablefunc extension. You'll have to create this extension one time for the database.


You can use the below code to create pivot table using cross tab:

create table test_Crosstab( section text,
<br/>status text,
<br/>count numeric)

<br/>insert into test_Crosstab values ( 'A','Active',1)
                <br/>,( 'A','Inactive',2)
                <br/>,( 'B','Active',4)
                <br/>,( 'B','Inactive',5)

select * from crosstab(
<br/>'select section
    <br/>from test_crosstab'
    <br/>)as ctab ("Section" text,"Active" numeric,"Inactive" numeric)

@Erwin Brandstetter 2019-02-27 19:07:12

This answer adds nothing over pre-existing answers.

@Milos 2017-02-24 12:31:41

Solution with JSON aggregation:

  section   text
, status    text
, ct        integer  -- don't use "count" as column name.

  ('A', 'Active', 1), ('A', 'Inactive', 2)
, ('B', 'Active', 4), ('B', 'Inactive', 5)
                   , ('C', 'Inactive', 7); 

SELECT section,
       (obj ->> 'Active')::int AS active,
       (obj ->> 'Inactive')::int AS inactive
FROM (SELECT section, json_object_agg(status,ct) AS obj
      FROM t
      GROUP BY section

@Jeremiah Peschka 2010-06-09 01:34:00

You can use the crosstab() function of the additional module tablefunc - which you have to install once per database. Since PostgreSQL 9.1 you can use CREATE EXTENSION for that:


In your case, I believe it would look something like this:

CREATE TABLE t (Section CHAR(1), Status VARCHAR(10), Count integer);

INSERT INTO t VALUES ('A', 'Active',   1);
INSERT INTO t VALUES ('A', 'Inactive', 2);
INSERT INTO t VALUES ('B', 'Active',   4);
INSERT INTO t VALUES ('B', 'Inactive', 5);

SELECT row_name AS Section,
       category_1::integer AS Active,
       category_2::integer AS Inactive
FROM crosstab('select section::text, status, count::text from t',2)
            AS ct (row_name text, category_1 text, category_2 text);

@Wim Verhavert 2010-08-05 09:59:12

In case you use a parameter in the crosstab query, you have to escape it properly. Example: (from above) say you want only the active ones: SELECT ... FROM crosstab('select section::text, status, count::text from t where status=''active''', 2) AS ... (notice the double quotes). In case the parameter is passed at runtime by the user (as a function parameter for example) you can say: SELECT ... FROM crosstab('select section::text, status, count::text from t where status=''' || par_active || '''', 2) AS ... (triple quotes here!). In BIRT this also works with the ? placeholder.

@LanceH 2010-06-09 01:33:08

Sorry this isn't complete because I can't test it here, but it may get you off in the right direction. I'm translating from something I use that makes a similar query:

select mt.section, mt1.count as Active, mt2.count as Inactive
from mytable mt
left join (select section, count from mytable where status='Active')mt1
on mt.section = mt1.section
left join (select section, count from mytable where status='Inactive')mt2
on mt.section = mt2.section
group by mt.section,
order by mt.section asc;

The code I'm working from is:

select m.typeID, m1.highBid, m2.lowAsk, m1.highBid - m2.lowAsk as diff, 100*(m1.highBid - m2.lowAsk)/m2.lowAsk as diffPercent
from mktTrades m
   left join (select typeID,MAX(price) as highBid from mktTrades where bid=1 group by typeID)m1
   on m.typeID = m1.typeID
   left join (select typeID,MIN(price) as lowAsk  from mktTrades where bid=0 group by typeID)m2
   on m1.typeID = m2.typeID
group by m.typeID, 
order by diffPercent desc;

which will return a typeID, the highest price bid and the lowest price asked and the difference between the two (a positive difference would mean something could be bought for less than it can be sold).

@Jeremiah Peschka 2010-06-09 01:41:02

You're missing a from clause, otherwise this is correct. The explain plans are wildly different on my system - the crosstab function has a cost of 22.5 while the LEFT JOIN approach is about 4 times as expensive with a cost of 91.38. It also produces about twice as many physical reads and performs hash joins - which can be quite expensive compared to other join types.

@LanceH 2010-06-09 02:40:13

Thanks Jeremiah, that's good to know. I've upvoted the other answer, but your comment is worth keeping so I won't delete this one.

Related Questions

Sponsored Content

21 Answered Questions

[SOLVED] Show tables in PostgreSQL

  • 2009-04-20 19:07:39
  • flybywire
  • 1455083 View
  • 1469 Score
  • 21 Answer
  • Tags:   postgresql

19 Answered Questions


7 Answered Questions

[SOLVED] How to set auto increment primary key in PostgreSQL?

  • 2011-10-10 20:56:40
  • mkn
  • 414692 View
  • 245 Score
  • 7 Answer
  • Tags:   sql postgresql

17 Answered Questions

[SOLVED] How can I get column names from a table in SQL Server?

7 Answered Questions

[SOLVED] How to exit from PostgreSQL command line utility: psql

25 Answered Questions

5 Answered Questions

[SOLVED] "use database_name" command in PostgreSQL

2 Answered Questions

PostgreSQL Crosstab Select Query

17 Answered Questions

[SOLVED] psql: FATAL: database "<user>" does not exist

  • 2013-07-13 19:18:24
  • Ryan Rich
  • 260749 View
  • 572 Score
  • 17 Answer
  • Tags:   postgresql psql

13 Answered Questions

[SOLVED] MySQL Query GROUP BY day / month / year

Sponsored Content