Оператор UNPIVOT

Как следует из названия оператора, UNPIVOT выполняет обратную по отношению к PIVOT операцию, т.е. представляет данные, записанные в строке таблицы, в одном столбце. В примере, рассмотренном в предыдущем параграфе, мы с помощью оператора PIVOT разворачивали в строку таблицу, полученную с помощью следующего запроса:

SELECT screen, AVG(price) avg_
FROM Laptop
GROUP BY screen;
mssql
🚫
[[ error ]]
[[ column ]]
[[ value ]]
screenavg_
11700
12960
141175
151050

В результате было получено следующее представление:

avg_11121415
average price70096011751050

Исходный результат мы можем получить, если применим к pivot-запросу unpivot-преобразование:

SELECT screen -- заголовок столбца, который будет содержать заголовки
-- строк исходной таблицы
,avg__ AS avg_
-- заголовок столбца, который будет содержать значения из строки исходной таблицы
from(
-- pivot-запрос из предыдущего примера
SELECT [avg_],
[11],[12],[14],[15]
FROM (SELECT 'average price' AS 'avg_', screen, price
FROM Laptop) x
PIVOT
(AVG(price)
FOR screen
IN([11],[12],[14],[15])
)
pvt
-- конец pivot-запроса
) pvt
UNPIVOT
(avg__ -- заголовок столбца, который будет содержать значения
-- из столбцов исходной таблицы, перечисленных ниже
FOR screen in([11],[12],[14],[15])
) unpvt;
mssql
🚫
[[ error ]]
[[ column ]]
[[ value ]]

Заметим, что имя avg_ нельзя использовать в операторе UNPIVOT, поскольку оно уже использовалось в операторе PIVOT, поэтому я использовал новое имя avg__, которому затем присвоил алиас, чтобы полностью воссоздать результат, полученный с помощью группировки.

Рассмотрим теперь более содержательный пример. Пусть требуется информацию о рейсе 1100 представить в следующем виде:

trip_nospecinfo
1100id_comp4
1100planeBoeing
1100town_fromRostov
1100town_toParis
1100time_out14:30:00
1100time_in17:50:00

Поскольку информация из строки таблицы трансформируется в столбец, то напрашивается использование оператора UNPIVOT. Здесь следует сделать одно замечание. Значения в этом столбце должны быть одного типа. Поскольку в этот столбец в нашем примере собираются значения из разных столбцов исходной таблицы, то нужно преобразовать их к единому типу. Более того, должны совпадать не только типы, но и размер.

Общим типом в нашем случае является строковый тип. Поскольку столбцы town_from и town_to уже имеют тип char(25), то приведем все к этому типу:

SELECT trip_no,CAST(id_comp AS CHAR(25)) id_comp,
CAST(plane AS CHAR(25))
plane,town_from,town_to,
CONVERT(CHAR(25),time_out, 108) time_out,
CONVERT(CHAR(25),time_in,108) time_in
FROM Trip
WHERE trip_no =1100;
mssql
🚫
[[ error ]]
[[ column ]]
[[ value ]]
trip_noid_compplanetown_fromtown_totime_outtime_in
11004BoeingRostovParis14:30:0017:50:00

Здесь мы заодно преобразовали время вылета/прилета, убрав из него составляющую даты:

CONVERT(CHAR(25),time_out, 108)

Остальное, я надеюсь, понятно из кода:

SELECT trip_no, spec, info FROM (
SELECT trip_no,CAST(id_comp AS CHAR(25)) id_comp,
CAST(plane AS CHAR(25)) plane,
CAST(town_from AS CHAR(25)) town_from,
CAST(town_to AS CHAR(25)) town_to,
CONVERT(CHAR(25),time_out, 108) time_out,
CONVERT(CHAR(25),time_in,108) time_in
FROM Trip
WHERE trip_no =1100
) x
UNPIVOT(
info
FOR spec IN(id_comp, plane, town_from, town_to, time_out, time_in)
) unpvt;
mssql
🚫
[[ error ]]
[[ column ]]
[[ value ]]

Столбец с именем spec используется для вывода названий параметров, а столбец info содержит сами параметры. Результат выполнения запроса уже был представлен в условии задачи.

Рекомендуемые упражнения: 146, 97

Пусть нам требуется повернуть строку, содержащую NULL-значение в одном из столбцов.

WITH utest AS
(SELECT 1 a, 2 b, NULL c)
SELECT * FROM utest;
mssql
🚫
[[ error ]]
[[ column ]]
[[ value ]]

Т.е. вместо результата

abc
12NULL
a1
b2
cNULL

Применим оператор UNPIVOT:

WITH utest AS
(SELECT 1 a, 2 b, NULL c)
SELECT col, value FROM utest
UNPIVOT (
value FOR col IN (a,b,c)
) AS unpvt;
mssql
🚫
[[ error ]]
[[ column ]]
[[ value ]]

Первая неожиданность - ошибка компиляции:

Тип столбца "c" конфликтует с типами других столбцов, указанных в списке UNPIVOT.

Это означает, что сервер неявно не преобразовал тип столбца “с”, содержащий NULL, к типу первых двух столбцов, которые могут быть оценены как целочисленные.

Давайте сделаем это явно:

WITH utest AS
(SELECT 1 a, 2 b, CAST(NULL AS INT) c)
SELECT col,value FROM utest
UNPIVOT (
value FOR col IN (a,b,c)
) AS unpvt;
mssql
🚫
[[ error ]]
[[ column ]]
[[ value ]]
colvalue
a1
b2

Вторая неожиданность - оказывается UNPIVOT игнорирует NULL-значения, не выводя их в результирующем наборе.

Первое, что приходит в голову всем, это заменить NULL каким-нибудь валидным значением, заведомо отсутствующим в столбце. Если, в соответствии с ограничениями предметной области, значения с не могут быть отрицательными, заменим NULL на -1:

WITH utest AS
(SELECT 1 a, 2 b,  COALESCE(CAST(NULL AS INT),-1) c)
SELECT col, value FROM utest
UNPIVOT (
value FOR col IN (a, b, c)
) AS unpvt;
mssql
🚫
[[ error ]]
[[ column ]]
[[ value ]]

 | col | value | |——-|———| | a | 1 | | b | 2 | | c | -1 |

Остался последний шаг, о котором многие забывают, решая задачи на сайте sql-ex.ru. А именно, обратное преобразование. Вместо этого пытаются подобрать такое значение, которое позволило бы “удовлетворить” систему проверки. Иногда это получается, например, если сравнение NULL и ’’ (пустой строки) оценивается на сайте как true. Но понятно, что на это полагаться не стоит. Итак, обратное пребразование:

WITH utest AS
(SELECT 1 a, 2 b,  COALESCE(CAST(NULL AS INT),-1) c)
SELECT col, NULLIF(value, -1) value FROM utest
UNPIVOT (
value FOR col IN (a, b, c)
) AS unpvt;
mssql
🚫
[[ error ]]
[[ column ]]
[[ value ]]
colvalue
a1
b2
cNULL

Здесь как нельзя более кстати пришлась функция NULLIF.