Engineering blog

SQL Pivot: Converting Rows to Columns

Share this post

Try this notebook in Databricks

UPDATED 11/10/2018

Pivot was first introduced in Apache Spark 1.6 as a new DataFrame feature that allows users to rotate a table-valued expression by turning the unique values from one column into individual columns.

The Apache Spark 2.4 release extends this powerful functionality of pivoting data to our SQL users as well. In this blog, using temperatures recordings in Seattle, we’ll show how we can use this common SQL Pivot feature to achieve complex data transformations.

Examining Summer Temperatures with Pivot

This summer in Seattle temperatures rose to uncomfortable levels, peaking to high 80s and 90, for nine days in July.

DateTemp (°F)
07-22-201886
07-23-201890
07-24-201891
07-25-201892
07-26-201892
07-27-201888
07-28-201885
07-29-201894
07-30-201889

Suppose we want to explore or examine if there were a historical trend in rising mercury levels. One intuitive way to examine and present these numbers is to have months as the columns and then each year’s monthly average highs in a single row. That way it will be easy to compare the temperatures both horizontally, between adjacent months, and vertically, between different years.

Now that we have support for PIVOT syntax in Spark SQL, we can achieve this with the following SQL query.

SELECT * FROM (
  SELECT year(date) year, month(date) month, temp
  FROM high_temps
  WHERE date BETWEEN DATE '2015-01-01' AND DATE '2018-08-31'
)
PIVOT (
  CAST(avg(temp) AS DECIMAL(4, 1))
  FOR month in (
    1 JAN, 2 FEB, 3 MAR, 4 APR, 5 MAY, 6 JUN,
    7 JUL, 8 AUG, 9 SEP, 10 OCT, 11 NOV, 12 DEC
  )
)
ORDER BY year DESC

The above query will produce a result like:

YEARJANFEBMARAPRMAYJUNEJULYAUGSEPTOCTNOVDEC
201849.745.854.058.670.871.982.879.1NULLNULLNULLNULL
201743.746.651.657.367.072.178.381.573.861.151.345.6
201649.153.656.465.968.873.176.079.569.660.656.041.9
201550.354.557.959.968.078.982.679.068.563.649.447.1

Well, looks like there are good years and bad years. The year 2016 seems a rather energy-friendly year.

Pivoting in SQL

Let’s take a closer look at this query to understand how it works. First, we need to specify the FROM clause, which is the input of the pivot, in other words, the table or subquery based on which the pivoting will be performed. In our case, we are concerned about the years, the months, and the high temperatures, so those are the fields that appear in the sub-query.

Second, let’s consider another important part of the query, the PIVOT clause. The first argument of the PIVOT clause is an aggregate function and the column to be aggregated. We then specify the pivot column in the FOR sub-clause as the second argument, followed by the IN operator containing the pivot column values as the last argument.

The pivot column is the point around which the table will be rotated, and the pivot column values will be transposed into columns in the output table. The IN clause also allows you to specify an alias for each pivot value, making it easy to generate more meaningful column names.

An important idea about pivot is that it performs a grouped aggregation based on a list of implicit group-by columns together with the pivot column. The implicit group-by columns are columns from the FROM clause that do not appear in any aggregate function or as the pivot column.

In the above query, with the pivot column being the column month and the implicit group-by column being the column year, the expression avg(temp) will be aggregated on each distinct value pair of (year, month), where month equals to one of the specified pivot column values. As a result, each of these aggregated values will be mapped into its corresponding cell of row year and column month.

It is worth noting that because of this implicit group-by, we need to make sure that any column that we do not wish to be part of the pivot output should be left out from the FROM clause, otherwise the query would produce undesired results.

Specifying Multiple Aggregate Expressions

The above example shows only one aggregate expression being used in the PIVOT clause, while in fact, users can specify multiple aggregate expressions if needed. Again, with the weather data above, we can list the maximum high temperatures along with the average high temperatures between June and September.

SELECT * FROM (
  SELECT year(date) year, month(date) month, temp
  FROM high_temps
  WHERE date BETWEEN DATE '2015-01-01' AND DATE '2018-08-31'
)
PIVOT (
  CAST(avg(temp) AS DECIMAL(4, 1)) avg, max(temp) max
  FOR month in (6 JUN, 7 JUL, 8 AUG, 9 SEP)
)
ORDER BY year DESC

In case of multiple aggregate expressions, the columns will be the Cartesian product of the pivot column values and the aggregate expressions, with the names as _.

yearJUN_avgJUN_maxJUL_avgJUL_maxAUG_avgAUG_maxSEP_avgSEP_max
201871.98882.89479.194NULLNULL
201772.19678.38781.59473.890
201673.19376.08979.59569.678
201578.99282.69579.09268.581

Grouping Columns vs. Pivot Columns

Now suppose we want to include low temperatures in our exploration of temperature trends from this table of daily low temperatures:

DateTemp (°F)
......
08-01-201859
08-02-201858
08-03-201859
08-04-201858
08-05-201859
08-06-201859
......

To combine this table with the previous table of daily high temperatures, we could join these two tables on the “Date” column. However, since we are going to use pivot, which performs grouping on the dates, we can simply concatenate the two tables using UNION ALL. And you’ll see later, this approach also provides us with more flexibility:

SELECT date, temp, 'H' as flag
FROM high_temps
UNION ALL
SELECT date, temp, 'L' as flag
FROM low_temps

Now let’s try our pivot query with the new combined table:

<br />SELECT * FROM (
  SELECT year(date) year, month(date) month, temp, flag `H/L`
  FROM (
    SELECT date, temp, 'H' as flag
    FROM high_temps
    UNION ALL
    SELECT date, temp, 'L' as flag
    FROM low_temps
  )
  WHERE date BETWEEN DATE '2015-01-01' AND DATE '2018-08-31'
)
PIVOT (
  CAST(avg(temp) AS DECIMAL(4, 1))
  FOR month in (6 JUN, 7 JUL, 8 AUG, 9 SEP)
)
ORDER BY year DESC, `H/L` ASC

As a result, we get the average high and average low for each month of the past 4 years in one table. Note that we need to include the column flag in the pivot query, otherwise the expression avg(temp) would be based on a mix of high and low temperatures.

yearH/LJUNJULAUGSEP
2018H71.982.879.1NULL
2018L53.458.558.5NULL
2017H72.178.381.573.8
2017L53.756.359.055.6
2016H73.176.079.569.9
2016L53.957.659.952.9
2015H78.982.679.068.5
2015L56.459.958.552.5

You might have noticed that now we have two rows for each year, one for the high temperatures and the other for low temperatures. That’s because we have included one more column, flag, in the pivot input, which in turn becomes another implicit grouping column in addition to the original column year.

Alternatively, instead of being a grouping column, the flag can also serve as a pivot column. So now we have two pivot columns, month and flag:

SELECT * FROM (
  SELECT year(date) year, month(date) month, temp, flag
  FROM (
    SELECT date, temp, 'H' as flag
    FROM high_temps
    UNION ALL
    SELECT date, temp, 'L' as flag
    FROM low_temps
  )
  WHERE date BETWEEN DATE '2015-01-01' AND DATE '2018-08-31'
)
PIVOT (
  CAST(avg(temp) AS DECIMAL(4, 1))
  FOR (month, flag) in (
    (6, 'H') JUN_hi, (6, 'L') JUN_lo,
    (7, 'H') JUL_hi, (7, 'L') JUL_lo,
    (8, 'H') AUG_hi, (8, 'L') AUG_lo,
    (9, 'H') SEP_hi, (9, 'L') SEP_lo
  )
)
ORDER BY year DESC

This query presents us with a different layout of the same data, with one row for each year, but two columns for each month.

yearJUN_hiJUN_loJUL_hiJUL_loAUG_hiAUG_loSEP_hiSEP_lo
201871.953.482.858.579.158.5NULLNULL
201772.153.778.356.381.559.073.855.6
201673.153.976.057.679.557.969.652.9
201578.956.482.659.979.058.568.552.5

What’s Next

To run the query examples used in this blog, please check the pivot SQL examples in this accompanying notebook.

Thanks to the Apache Spark community contributors for their contributions!

Try Databricks for free

Related posts

Engineering blog

SQL Pivot: Converting Rows to Columns

November 1, 2018 by MaryAnn Xue in Engineering Blog
Try this notebook in Databricks UPDATED 11/10/2018 Pivot was first introduced in Apache Spark 1.6 as a new DataFrame feature that allows users...
See all Engineering Blog posts