Equivalent to unpivot() in PostgreSQL

Viewed 51573

Is there a unpivot equivalent function in PostgreSQL?

9 Answers

You could use VALUES() and JOIN LATERAL to unpivot the columns.

Sample data:

CREATE TABLE test(id int, a INT, b INT, c INT);
INSERT INTO test(id,a,b,c) VALUES (1,11,12,13),(2,21,22,23),(3,31,32,33);

Query:

SELECT t.id, s.col_name, s.col_value
FROM test t
JOIN LATERAL(VALUES('a',t.a),('b',t.b),('c',t.c)) s(col_name, col_value) ON TRUE;

DBFiddle Demo

Using this approach it is possible to unpivot multiple groups of columns at once.

EDIT

Using Zack's suggestion:

SELECT t.id, col_name, col_value
FROM test t
CROSS JOIN LATERAL (VALUES('a', t.a),('b', t.b),('c',t.c)) s(col_name, col_value);

<=>

SELECT t.id, col_name, col_value
FROM test t
,LATERAL (VALUES('a', t.a),('b', t.b),('c',t.c)) s(col_name, col_value);

db<>fiddle demo

Great article by Thomas Kellerer found here

Unpivot with Postgres

Sometimes it’s necessary to normalize de-normalized tables - the opposite of a “crosstab” or “pivot” operation. Postgres does not support an UNPIVOT operator like Oracle or SQL Server, but simulating it, is very simple.

Take the following table that stores aggregated values per quarter:

create table customer_turnover
(
  customer_id   integer,
  q1            integer,
  q2            integer,
  q3            integer,
  q4            integer
);

And the following sample data:

customer_id | q1  | q2  | q3  | q4 
------------+-----+-----+-----+----
          1 | 100 | 210 | 203 | 304
          2 | 150 | 118 | 422 | 257
          3 | 220 | 311 | 271 | 269

But we want the quarters to be rows (as they should be in a normalized data model).

In Oracle or SQL Server this could be achieved with the UNPIVOT operator, but that is not available in Postgres. However Postgres’ ability to use the VALUES clause like a table makes this actually quite easy:

select c.customer_id, t.*
from customer_turnover c
  cross join lateral (
     values 
       (c.q1, 'Q1'),
       (c.q2, 'Q2'),
       (c.q3, 'Q3'),
       (c.q4, 'Q4')
  ) as t(turnover, quarter)
order by customer_id, quarter;

will return the following result:

customer_id | turnover | quarter
------------+----------+--------
          1 |      100 | Q1     
          1 |      210 | Q2     
          1 |      203 | Q3     
          1 |      304 | Q4     
          2 |      150 | Q1     
          2 |      118 | Q2     
          2 |      422 | Q3     
          2 |      257 | Q4     
          3 |      220 | Q1     
          3 |      311 | Q2     
          3 |      271 | Q3     
          3 |      269 | Q4     

The equivalent query with the standard UNPIVOT operator would be:

select customer_id, turnover, quarter
from customer_turnover c
  UNPIVOT (turnover for quarter in (q1 as 'Q1', 
                                    q2 as 'Q2', 
                                    q3 as 'Q3',
                                    q4 as 'Q4'))
order by customer_id, quarter;

Pulling slightly modified content from the link in the comment from @a_horse_with_no_name into an answer because it works:

Installing Hstore
If you don't have hstore installed and are running PostgreSQL 9.1+, you can use the handy

CREATE EXTENSION hstore;

For lower versions, look for the hstore.sql file in share/contrib and run in your database.

Assuming that your source (e.g., wide data) table has one 'id' column, named id_field, and any number of 'value' columns, all of the same type, the following will create an unpivoted view of that table.

CREATE VIEW vw_unpivot AS 
SELECT id_field, (h).key AS column_name, (h).value AS column_value
  FROM (
    SELECT id_field, each(hstore(foo) - 'id_field'::text) AS h 
      FROM zcta5 as foo  
  ) AS unpiv ; 

This works with any number of 'value' columns. All of the resulting values will be text, unless you cast, e.g., (h).value::numeric.

Just use JSON:

with data (id, name) as (
  values (1, 'a'), (2, 'b')
)
select t.*
from data, lateral jsonb_each_text(to_jsonb(data)) with ordinality as t
order by data.id, t.ordinality;

This yields

|key |value|ordinality|
|----|-----|----------|
|id  |1    |1         |
|name|a    |2         |
|id  |2    |1         |
|name|b    |2         |

dbfiddle

Depending on what you want to do... something like this can be helpful.

    with wide_table as (
    select 1 a, 2 b, 3 c
    union all
    select 4 a, 5 b, 6 c
)
select unnest(array[a,b,c]) from wide_table

You can use FROM UNNEST() array handling to UnPivot a dataset, tandem with a correlated subquery (works w/ PG 9.4).

FROM UNNEST() is more powerful & flexible than the typical method of using FROM (VALUES .... ) to unpivot datasets. This is b/c FROM UNNEST() is variadic (with n-ary arity). By using a correlated subquery the need for the lateral ORDINAL clause is eliminated, & Postgres keeps the resulting parallel columnar sets in the proper ordinal sequence.

This is, BTW, FAST -- in practical use spawning 8 million rows in < 15 seconds on a 24-core system.

WITH _students AS ( /** CTE **/
                  SELECT * FROM
                    (   SELECT 'jane'::TEXT ,'doe'::TEXT , 1::INT 
                         UNION
                        SELECT 'john'::TEXT ,'doe'::TEXT , 2::INT 
                         UNION
                        SELECT 'jerry'::TEXT ,'roe'::TEXT , 3::INT 
                         UNION
                        SELECT 'jodi'::TEXT ,'roe'::TEXT , 4::INT 
                    ) s ( fn, ln, id )
) /** end WITH **/   
SELECT s.id
 , ax.fanm  -- field labels, now expanded to two rows
 , ax.anm  -- field data, now expanded to two rows
 , ax.someval -- manually incl. data
 , ax.rankednum -- manually assigned ranks
 ,ax.genser -- auto-generate ranks
FROM _students s
,UNNEST /** MULTI-UNNEST() BLOCK **/
    (
        ( SELECT ARRAY[ fn, ln ]::text[] AS anm -- expanded into two rows by outer UNNEST()
                  /** CORRELATED SUBQUERY **/
                 FROM _students s2 WHERE s2.id = s.id -- outer relation
         )   
   
        ,( /** ordinal relationship preserved in variadic UNNEST() **/
         SELECT ARRAY[ 'first name', 'last name' ]::text[] -- exp. into 2 rows
                  AS fanm 
         )  
     
        ,( SELECT ARRAY[ 'z','x','y'] -- only 3 rows gen'd, but ordinal rela. kept
                 AS someval
        ) 
   
        ,( SELECT ARRAY[ 1,2,3,4,5   ] -- 5 rows gen'd, ordinal rela. kept.
                  AS rankednum
         ) 

        ,( SELECT ARRAY( /** you may go wild ... **/
                         SELECT generate_series(1, 15, 3 ) 
                         AS genser
                         )
          )
        


   ) ax (  anm, fanm, someval, rankednum , genser  )

;
 

RESULT SET:

+--------+----------------+-----------+----------+---------+-------
|   id   |   fanm         |   anm     | someval  |rankednum| [ etc. ]  
+--------+----------------+-----------+----------+---------+-------
|   2    |   first name   |   john    |   z      |    1    |    .
|   2    |   last name    |   doe     |   y      |    2    |    .
|   2    |   [null]       |  [null]   |   x      |    3    |    .
|   2    |   [null]       |  [null]   |  [null]  |    4    |    .
|   2    |   [null]       |  [null]   |  [null]  |    5    |    .
|   1    |   first name   |   jane    |   z      |    1    |    .
|   1    |   last name    |   doe     |   y      |    2    |    .
|   1    |                |           |   x      |    3    |    .
|   1    |                |           |          |    4    |    .
|   1    |                |           |          |    5    |    .
|   4    |   first name   |   jodi    |   z      |    1    |    .
|   4    |   last name    |   roe     |   y      |    2    |    .
|   4    |                |           |   x      |    3    |    .
|   4    |                |           |          |    4    |    .
|   4    |                |           |          |    5    |    .
|   3    |   first name   |   jerry   |   z      |    1    |    .
|   3    |   last name    |   roe     |   y      |    2    |    .
|   3    |                |           |   x      |    3    |    .
|   3    |                |           |          |    4    |    .
|   3    |                |           |          |    5    |    .
+--------+----------------+-----------+----------+---------+ ----   
Related