19

I have a view which looks like this

          value1count     value2count value3count
          ----------------------------------------
             25              35          55

I need to transpose the column header into rows and so I need it to look like

          Values              Count
         -----------------------------
           value1count         25
           value2count         35
           value3count         55

I can do this by selecting individual column names as first column and data as second column and then do a union of the same for all columns.

Is there a better way to do this? I am using PosgreSQL 8.1 and so don't have pivot operators to work with.

Thanks for your response in advance.

Jonathan Hall
  • 75,165
  • 16
  • 143
  • 189
cableload
  • 4,215
  • 5
  • 36
  • 62
  • yes we are upgrading to the latest version..probably by end of summer....Thanks – cableload May 16 '12 at 19:02
  • Columns like that (and questions like that) usually indicate a flawed design. You mention a VIEW involved so it might not be the case with your tables. But do consider a re-design if that view reflects the actual table structure –  May 16 '12 at 20:41

7 Answers7

49

Crosstab only does the reverse of what you need, but this should help you:

First create the unnest() function that is included in 8.4, see here for instructions.

Then you can do this (based on this post):

SELECT
   unnest(array['value1Count', 'value2Count', 'value3Count']) AS "Values",
   unnest(array[value1Count, value2Count, value3Count]) AS "Count"
FROM view_name
ORDER BY "Values"

I can verify that this works in 8.4, but because I don't have 8.1, I can't promise it will work the same.

Community
  • 1
  • 1
PinnyM
  • 35,165
  • 3
  • 73
  • 81
  • 3
    This is brilliant..worked fine in 8.1..my union query took 10 seconds and this unnest query just took 3 seconds..Thanks a lot PinnyM. – cableload May 16 '12 at 20:27
  • Just tried it in postgresql 11 and it worked out of the box. – Dino Dini Feb 03 '19 at 10:15
  • SELECT unnest(array['x1','x2','x3','x4','x5','x6','x7','x8','x9','x10','x11','x12','x13','x14','x15','x16']) opciones, unnest(array[x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16]) marcado FROM control_xopcion_table where handler_id = 76 and idemergencia = 2573 – JAGJ jdfoxito Jun 11 '20 at 08:35
  • @PinnyM let's say we have more columns that get added to the table in the future so is there a way we can make the arrays to be populated more dynamically? – Adarsh Ravi Jun 21 '21 at 20:06
  • @AdarshRavi you can use the hstore approach mentioned in the answer below this one. – PinnyM Jun 22 '21 at 03:16
  • How do I do from the reverse operation of transpose for this example? – Akira Chen Jan 05 '22 at 02:32
  • @AkiraChen this is what the crosstab function can help with. – PinnyM Jan 05 '22 at 04:08
9

I achieved your goal by using hstore's functionalities :

SELECT (x).key, (x).value
FROM
  ( SELECT EACH(hstore(t)) as x
    FROM   t
  ) q;

May you have multiple rows in your "to-be-exploded" view or table (referred here as t), you might need to insert an additionnal identifier in the intermediate table q, for example:

SELECT id, (x).key, (x).value
FROM
  ( SELECT id, EACH(hstore(t)) as x
    FROM   t
  ) q;

Reference: hstore documentation

Mickaël Le Baillif
  • 2,027
  • 16
  • 22
  • +1, The advantage here is avoiding the need to name your columns in your query. Apparently [it doesn't perform quite as well as unnest](http://www.postgresonline.com/journal/archives/283-Unpivoting-data-in-PostgreSQL.html), but not to a significant degree. – PinnyM Jun 02 '17 at 17:32
  • This is a brilliant solution, thank you! The only problem is that the column ordering gets messed up. Do you have any idea how to work around that? – David Dec 14 '17 at 13:58
5

I was looking to do something similar to this to more easily process table information from a bash script. Turns out to be very easy to tell psql to display table columns as rows:

psql mydbname -x -A -F= -c "select * from blah where id=123"
  • The -x is what pivots the output.
  • The -A removes extra spacing.
  • The -F= replaces | with = between the column name and the value.

This of course wont work in SQL, it only modifies how psql happens to format output.

Stéphane
  • 19,459
  • 24
  • 95
  • 136
  • 2
    Thanks! And if you need that behaviour during an interactive psql session, you can use `\x on` or even `\x auto` to switch to that mode whenever output table would be too wide for your terminal. – geekQ Jan 27 '21 at 13:39
1

I had a similar situation. I wrapped my query in a with statement and then did a bunch of UNION ALLs for each row. In my situation, if I had multiple records, the ncm_id would be different, so I went ahead and added that to my column list in my result set. This may not be the best way to do this, but it worked for my use case.

 WITH query_a AS (
     SELECT
       fin_item.item_number || ' - ' || fin_item.item_descrip1 fin_item,
       fin_ls.ls_number,
       ls_sort.sortby_employeeid,
       ls_sort.ncm_id,
       ls_sort.created_at,
       ls_sort.updated_at,
       ls_sort.sort_qty,
       ls_sort.initial_scan_time,
       ls_sort.ncm_scan_time,
       ls_sort.badge_scan_time,
       ls_sort.computer_name,
       ls_sort.number_of_ops,
       ls_sort.ncm_item_scan_time,
       sort_by.name sort_by,
       tblncm.instructions,
       tblncm.ncm_comments
     FROM public.item AS fin_item
       INNER JOIN public.ls AS fin_ls ON fin_item.item_id = fin_ls.ls_item_id
       INNER JOIN stone.ls_sort ON fin_ls.ls_id = ls_sort.ls_id
       INNER JOIN stone.vw_mssql_employees AS sort_by ON ls_sort.sortby_employeeid = sort_by.employeeid
       INNER JOIN stone.tblncm ON ls_sort.ncm_id = tblncm.ncm_id
       LEFT JOIN stone.equips AS mach_equips ON ls_sort.mach_equip_id = mach_equips.id
       LEFT JOIN stone.equips AS mold_equips ON ls_sort.mold_equip_id = mold_equips.id
     WHERE 1 = 1
           AND fin_ls.ls_number ILIKE 'S143027526190' || '%'
 )
   SELECT *
   FROM (
     (SELECT 'fin_item' my_column, fin_item::TEXT my_value, ncm_id::TEXT my_ncm FROM query_a)
      UNION ALL
     (SELECT 'ls_number' my_column, ls_number::TEXT my_value, ncm_id::TEXT my_ncm FROM query_a)
      UNION ALL
     (SELECT 'sortby_employeeid' my_column, sortby_employeeid::TEXT my_value, ncm_id::TEXT my_ncm FROM query_a)
      UNION ALL
     (SELECT 'ncm_id' my_column, ncm_id::TEXT my_value, ncm_id::TEXT my_ncm FROM query_a)
      UNION ALL
     (SELECT 'created_at' my_column, created_at::TEXT my_value, ncm_id::TEXT my_ncm FROM query_a)
      UNION ALL
     (SELECT 'updated_at' my_column, updated_at::TEXT my_value, ncm_id::TEXT my_ncm FROM query_a)
      UNION ALL
     (SELECT 'sort_qty' my_column, sort_qty::TEXT my_value, ncm_id::TEXT my_ncm FROM query_a)
      UNION ALL
     (SELECT 'initial_scan_time' my_column, initial_scan_time::TEXT my_value, ncm_id::TEXT my_ncm FROM query_a)
      UNION ALL
     (SELECT 'ncm_scan_time' my_column, ncm_scan_time::TEXT my_value, ncm_id::TEXT my_ncm FROM query_a)
      UNION ALL
     (SELECT 'badge_scan_time' my_column, badge_scan_time::TEXT my_value, ncm_id::TEXT my_ncm FROM query_a)
      UNION ALL
     (SELECT 'computer_name' my_column, computer_name::TEXT my_value, ncm_id::TEXT my_ncm FROM query_a)
      UNION ALL
     (SELECT 'number_of_ops' my_column, number_of_ops::TEXT my_value, ncm_id::TEXT my_ncm FROM query_a)
      UNION ALL
     (SELECT 'ncm_item_scan_time' my_column, ncm_item_scan_time::TEXT my_value, ncm_id::TEXT my_ncm FROM query_a)
      UNION ALL
     (SELECT 'sort_by' my_column, sort_by::TEXT my_value, ncm_id::TEXT my_ncm FROM query_a)
      UNION ALL
     (SELECT 'instructions' my_column, instructions::TEXT my_value, ncm_id::TEXT my_ncm FROM query_a)
      UNION ALL
     (SELECT 'ncm_comments' my_column, ncm_comments::TEXT my_value, ncm_id::TEXT my_ncm FROM query_a)
   ) as query_guy
 ORDER BY my_ncm;
Daniel L. VanDenBosch
  • 2,350
  • 4
  • 35
  • 60
0

I could be not understanding...but the way I did this was to also select DISTINCT then select the average columns values I needed. Like this:

SELECT DISTINCT contributing_factor_vehicle_1, AVG(number_of_cyclist_injured) FROM "table1".motor_vehicle_collisions_crashes 
GROUP BY contributing_factor_vehicle_1
ORDER BY avg(number_of_cyclist_injured) desc

That's what created something like so: enter image description here

This was NYC motor vehicle crash data btw.

Kyle Pennell
  • 5,747
  • 4
  • 52
  • 75
0

Here's a way of using hstore, much like Mickaël Le Baillif's answer, but a little bit more concise, and allows you to choose a subset of the columns dynamically, and shows how you can include more regular "hard coded" columns, like an ID, that tend to be required in more realistic cases.

-- Example wide data, similar to the question, but with an extra "id" column
WITH example_wide_data("id", "value1count", "value2count", "value3count") AS (
  VALUES 
    (1, 4, 5, 6),
    (2, 8, 9, 10)
)

SELECT
  id,
  r.key AS value_name,
  r.value AS value
FROM
  example_wide_data w,
  each(hstore(w.*)) AS r(key, value)
WHERE
  -- Chooses the columns to transpose dynamically
  -- Difference cases will likely need a different condition
  r.key ~ '^value[0-9]+count$';

Note there is a downside to the hstore approach: it casts the values to text along the way, so you might need to cast the values back to another type, depending on your use case.

Michal Charemza
  • 25,940
  • 14
  • 98
  • 165
0

In reading these answers, I feel that using WITH and UNION would be way simpler; No additional functions or stuff:

WITH tmp AS (SELECT val1, val2, val3 FROM view_name) 
   SELECT val1 FROM tmp UNION
   SELECT val2 FROM tmp UNION
   SELECT val3 FROM tmp;
TheMadsen
  • 196
  • 1
  • 11