57

Suppose I have a table in Postgres called listings that looks like this:

id neighborhood bedrooms price
1 downtown 0 189000
2 downtown 3 450000
3 riverview 1 300000
4 riverview 0 250000
5 downtown 1 325000
6 riverview 2 350000

etc.

How do I write a crosstab query that shows the average price per bedrooms as the columns and neighborhoods as the rows?

The output of the query should have the following format:

neighborhood 0 1 2 3
downtown 189000 325000 - 450000
riverview 250000 300000 350000 -

etc.

0

3 Answers 3

47

First compute the average with the aggregate function avg():

SELECT neighborhood, bedrooms, avg(price)
FROM   listings
GROUP  BY 1,2
ORDER  BY 1,2;

Then feed the result to the crosstab() function (provided by the additional module tablefunc). Cast the avg to int if you want rounded results as displayed:

SELECT *
FROM   crosstab(
   'SELECT neighborhood, bedrooms, avg(price)::int
    FROM   listings
    GROUP  BY 1, 2
    ORDER  BY 1, 2;'

  , $$SELECT unnest('{0,1,2,3}'::int[])$$
   ) AS ct ("neighborhood" text, "0" int, "1" int, "2" int, "3" int);

fiddle

Detailed instructions:

The same can be achieved with the aggregate FILTER clause. A bit simpler, and doesn't need an additional module, but typically slower. Related answer with side-by-side solutions:

Sign up to request clarification or add additional context in comments.

2 Comments

@Avishai: (Fully functional) syntax shorthand with positional references, short for GROUP BY neighborhood, bedrooms
The need to input {0,1,2,3} is unfortunate, but the 'detailed instructions' don't avoid this either.
41

Another solution that implement with filter:

SELECT neighborhood,
   avg(price) FILTER (WHERE bedrooms = 0) AS "0",
   avg(price) FILTER (WHERE bedrooms = 1) AS "1",
   avg(price) FILTER (WHERE bedrooms = 2) AS "2",
   avg(price) FILTER (WHERE bedrooms = 3) AS "3"
FROM listings
GROUP BY neighborhood;

Comments

24

The best way to build pivot tables in Postgres are CASE expressions.

SELECT neighborhood,
       round(avg((CASE WHEN bedrooms = 0 THEN price END)), 2) AS "0",
       round(avg((CASE WHEN bedrooms = 1 THEN price END)), 2) AS "1",
       round(avg((CASE WHEN bedrooms = 2 THEN price END)), 2) AS "2",
       round(avg((CASE WHEN bedrooms = 3 THEN price END)), 2) AS "3"
FROM listings
GROUP BY neighborhood;

Running this on the question data yields

NEIGHBORHOOD                  0          1          2          3
-------------------- ---------- ---------- ---------- ----------
downtown                 256888     334000       NULL       NULL
riverview                  NULL     505000       NULL       NULL

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.