Участник:ArmorAdmin/PIVOT
Содержание
PIVOT — поворот таблицы в T-SQL
- Автор(ы): Чобиток Василий, 14 мая 2010
- В этой статье: о применении инструкций PIVOT и UNPIVOT языка T-SQL для разворота табличных данных.
Лирическое вступление
Решив возобновить свои навыки владения SQL, продолжил начатое когда-то решение упражнений на замечательном сайте sql-ex.ru. В отличие от многих других ресурсов, где проводится тестирование путем выбора варианта среди ответов на вопрос, здесь решением является собственный SQL-запрос, который должен выдать верный набор данных на тестовой базе данных.
В одной из задач возникла необходимость отобразить результирующий набор данных зеркально относительно диагонали. На практике мне не довелось сталкиваться со случаями, когда подобный разворот необходимо делать именно средствами SQL. Обычно это решается несколько проще уже на наборе данных, который был возвращён средствами SQL. Хотя, слышал, что есть любители задавать подобную задачку на собеседованиях...
На sql-ex используется СУБД MS SQL Server, работающая с расширенным вариантом SQL — Transact-SQL (T-SQL). Гугль на запрос «T-SQL повернуть таблицу» сразу подсказал, что существует инструкция pivot, а последующее изучение этой темы — unpivot.
Ужас! Таково было моё впечатление после изучения документации и примеров использования. Синтаксис специфический и интуитивно непонятный, документация и имеющиеся примеры из одной колонки делают одну строку и наоборот. А как быть с несколькими сроками и столбцами? В редких случаях имеем примеры нескольких строк и колонок в результате, но полученных агрегатными функциями. А что делать, если меня в конкретном случае агрегирование не интересует? «Ну нипанятна!»
Скажу откровенно, как работает pivot, к моменту написания этих строк я так до конца и не разобрался. Тем не менее, попробую в процессе написания понять сам и как можно проще объяснить читателям.
Формулируем задачу
В общем случае задача достаточно проста для понимания. Из определенного исходного набора данных получить результирующий, зеркально отображенный относительно диагонали, например:
Исходный набор Результирующий набор ------------------------------ ------------------------ Фамилия Г.р. Пол Рост Чел.1 Чел.2 Чел.3 ------------------------------ ------------------------ Иванов 1972 м 176 Иванов Петров Сидорова Петров 1981 м 181 1972 1981 1990 Сидорова 1990 ж 168 м м ж 176 181 168
Мы видим, что есть набор данных, в котором число колонок не совпадает с числом строк (4×3), при этом колонки превращаются в строки и наоборот (3×4).
Надеюсь, что к концу статьи мы сможем проделать подобную операцию. К сожалению, одной простой командой такая транспозиция не произойдет.
Примеры будут приводится по структуре и данным из тестовой БД сайта sql-ex (другой возможности проверить запросы на СУБД MS SQL Server у меня просто нет).
Начнём с конца, с освоения unpivot, он мне показался проще.
UNPIVOT
UNPIVOT используется совместно с инструкцией SELECT и позволяет строку с данными развернуть в виде колонки.
Например, есть таблица Product, содержащая информацию о производителе, номере и типе продукции:
Product ------------------ maker varchar(10) model varchar(50) type varchar(50)
При выполнении следующего простейшего запроса:
select maker, model, type from product where model = '1276'
получим:
maker model type A 1276 Printer
Все же для начала начнём с одной строчки. Сразу можно догадаться, что развороту данных помешает различный тип данных полей. Поэтому предыдущий запрос слегка изменяется, поле maker приводится к типу остальных полей — cast(maker as varchar(50))
. Теперь ничто не мешает совершить разворот:
select aData from (
-- Это предыдущий запрос с приведенным типом поля maker
select cast(maker as varchar(50)) maker, model, type
from product where model = '1276'
) as t
unpivot (
aData for fields in (maker, model, type)
) as unpvt
В результате выполнения этого, пока еще непонятного, запроса получим:
aData A 1276 Printer
Т. е. строка развернулась и стала вертикально. Имевшийся ранее селектовый запрос обёрнут новым, в котором присутствует блок unpivot. Синтаксис этого блока можно описать следующим образом:
unpivot(
<Поле1>
for <Поле2>
in (<ПереченьПолей>)
)
где:
- <Поле1> — имя поля внешнего запроса, в которое попадут «повёрнутые» данные. В примере было задано имя поля aData и выведены его значения (
select aData from ...
); - <Поле2> — имя поля, содержащего имена полей вложенного запроса. Это поле может быть выведено отдельной колонкой;
- <ПереченьПолей> — перечень имён полей вложенного запроса, выводимых в результирующую колонку. Здесь могут быть перечислены все или часть полей вложенного запроса, которые необходимо вывести в результирующую колонку.
Как можно догадаться, изменение первой строчки запроса
select fields, aData from ...
даст следующий результат:
fields aData maker A model 1276 type Printer
Тот же результат можно получить запросом select * from ...
, только первой будет выведена колонка aData.
Теперь рассмотрим работу с несколькими записями вложенного запроса. Для понимания вполне хватит двух. Изменим первичный запрос:
select cast(maker as varchar(50)) maker, model, type
from product where model in ('1276', '2113')
Тестовые данные:
maker model type A 1276 Printer E 2113 PC
В поворачивающем запросе изменился только вложенный подзапрос t:
select aData from (
select cast(maker as varchar(50)) maker, model, type
from product where model in ('1276', '2113')
) as t
unpivot (
aData for fields in (maker, model, type)
) as unpvt
В результате его выполнения получим:
aData A 1276 Printer E 2113 PC
Явная бессмыслица — данные в таком виде вряд ли применимы (что получится, если добавить в конце запроса сортировку? Например: order by fields
).
Чтобы данным придать осмысленность, попробуем их представить в таком виде:
model aData 1276 A 1276 Printer 2113 E 2113 PC
Здесь для каждой модели в отдельную колонку выведены характеристики «производитель» и «тип».
Если первую строку запроса изменить, добавив в нее поле model из вложенного запроса:
select model, aData from ...
то при выполнении запроса возникнет ошибка — поле «модель» участвует в развороте данных и не может быть выведено в отдельную колонку. Что делать? Всего лишь исключить поле model из перечня в блоке unpivot. Получим следующий рабочий запрос:
select model, aData from (
select cast(maker as varchar(50)) maker, model, type
from product where model in ('1276', '2113')
) as t
unpivot (
aData for fields in (maker, type)
) as unpvt
Если в первую строку запроса добавить еще поле fields и изменить имена полей, получим:
select model, fields, aData from (
select cast(maker as varchar(50)) as [производитель], model, type as [тип]
from product where model in ('1276', '2113')
) as t
unpivot (
aData for fields in ([производитель], [тип])
) as unpvt
и результат запроса:
model fields aData 1276 производитель A 1276 тип Printer 2113 производитель E 2113 тип PC
Таким образом, UNPIVOT
позволяет:
- полностью или частично развернуть запись (строку) и представить её в вертикальном, колоночном виде;
- вывести в отдельной колонке имена полей или их синонимы, заданные в запросе;
- выводить поля из внутреннего запроса в виде отдельной колонки с тем ограничением, что эти поля не должны участвовать в развороте данных (отсутствовать в перечислении полей в блоке unpivot).
PIVOT
PIVOT является обратным по отношению к UNPIVOT, т. е. столбцы поворачивает в строки, но делает это несколько иначе, с использованием агрегатной функции.
Синтаксис подобен:
pivot(
aggregation(<Поле1>)
for <Поле2>
in (<ПереченьПолей>)
)
Особенности синтаксиса:
- в отличие от unpivot, в котором <Поле1> было полем результирующего набора данных, здесь <Поле1> — имя поля, которое должно быть в исходном наборе;
- в обязательном порядке по полю <Поле1> должна выполняться агрегация (sum, count, avg и т. п.). Странная особенность, её рассмотрим позднее;
- <Поле2> — имя поля исходного набора данных, значения которого будут выступать в роли колонок итогового набора данных.
Рассмотрим пример.
select * from product
pivot (
count(model) for maker in ([A], [B], [D])
) as pvt
Результат выполнения запроса:
type A B D Laptop 2 1 0 PC 2 1 0 Printer 3 0 2
Используемые в блоке pivot запроса поля model и maker, это поля, которые содержит исходный набор данных, таблица product. В перечислении maker in ([A], [B], [D])
значения «A», «B» и «D» — названия тех производителей из колонки maker, данные по которым необходимо вывести в виде отдельных колонок.
Здесь сразу заметно существенное ограничение в использовании pivot — с его помощью поворачивается не любой набор данных, а тот, из которого мы можем получить заранее оговорённый набор колонок (кварталы года, дни месяца, перечень конкретных компаний, сотрудников и т. п.).
При ближайшем рассмотрении тестового примера становится понятно, что в отличие от unpivot, осуществляющего «чистый» разворот, pivot в первую очередь предназначен для создания таких себе отчетов в более удобочитаемой форме, поэтому в нем и присутствует необходимость использования агрегатной функции.
В начале непонятно, откуда в примере взялась колонка type, если в запросе мы её нигде не использовали? Pivot делает отчет по тому набору данных, который ему передан. В примере он получил в качестве набора данных просто таблицу product, которая содержит три поля maker, model и type. Мы указали просчитать количество моделей по таким-то производителям, но поскольку в наборе данных есть еще поля, то pivot делает группировку и по всем остальным полям, оставляя их в итоговом наборе данных.
Т.е. с точки зрения полученных данных наш запрос полностью аналогичен такому:
select maker, type, count(model) models
from product
where maker in ('A', 'B', 'D')
group by maker, type
отличается лишь представление данных:
maker type models A Laptop 2 B Laptop 1 A PC 2 B PC 1 A Printer 3 D Printer 2
Как быть, если мы хотим выбрать итог по производителям без учёта типа продукции? Ограничить набор данных требуемыми полями. Получим:
select 'Число моделей:' as [Производитель:], * from (
select maker, model from Product
where maker in ('A', 'B', 'D')
) as pr
pivot (
count(model) for maker in ([A], [B], [D])
) as pvt
Заодно я сразу ограничил набор исходных данных, с которыми мы работаем (maker in ('A', 'B', 'D')
). В результате получим:
Производитель: A B D Число моделей: 7 2 2
Для придания человекочитаемости результату запроса я нарушил принципы нормализации, в колонке «Производитель:» внесено значение «Число моделей:» :-)
Если не удовлетворяют имена колонок, полученные по значениям из исходного набора данных, то их можно переименовать обычным способом:
select [A] as 'Произв. 1', [B] as 'Произв. 2', [D] as 'Произв. 3' from...