SQL Server: столбцы в строки

129

Ищете элегантное (или любое) решение для преобразования столбцов в строки.

Вот пример: у меня есть таблица со следующей схемой:

[ID] [EntityID] [Indicator1] [Indicator2] [Indicator3] ... [Indicator150]

Вот что я хочу получить в результате:

[ID] [EntityId] [IndicatorName] [IndicatorValue]

И значения результата будут:

1 1 'Indicator1' 'Value of Indicator 1 for entity 1'
2 1 'Indicator2' 'Value of Indicator 2 for entity 1'
3 1 'Indicator3' 'Value of Indicator 3 for entity 1'
4 2 'Indicator1' 'Value of Indicator 1 for entity 2'

И так далее..

Имеет ли это смысл? Есть ли у вас какие-либо предложения о том, где искать и как это сделать на T-SQL?

Сергей
источник
2
Вы уже изучили Pivot / Unpivot ?
Джош Джей
В конце концов, это был раствор голубоногих. Элегантный и функциональный. Всем большое спасибо.
Сергей

Ответы:

249

Вы можете использовать функцию UNPIVOT для преобразования столбцов в строки:

select id, entityId,
  indicatorname,
  indicatorvalue
from yourtable
unpivot
(
  indicatorvalue
  for indicatorname in (Indicator1, Indicator2, Indicator3)
) unpiv;

Обратите внимание, что типы данных столбцов, которые вы отменяете, должны быть одинаковыми, поэтому вам, возможно, придется преобразовать типы данных перед применением отмены.

Вы также можете использовать CROSS APPLYUNION ALL для преобразования столбцов:

select id, entityid,
  indicatorname,
  indicatorvalue
from yourtable
cross apply
(
  select 'Indicator1', Indicator1 union all
  select 'Indicator2', Indicator2 union all
  select 'Indicator3', Indicator3 union all
  select 'Indicator4', Indicator4 
) c (indicatorname, indicatorvalue);

В зависимости от вашей версии SQL Server вы даже можете использовать CROSS APPLY с предложением VALUES:

select id, entityid,
  indicatorname,
  indicatorvalue
from yourtable
cross apply
(
  values
  ('Indicator1', Indicator1),
  ('Indicator2', Indicator2),
  ('Indicator3', Indicator3),
  ('Indicator4', Indicator4)
) c (indicatorname, indicatorvalue);

Наконец, если у вас есть 150 столбцов, которые нужно развернуть, и вы не хотите жестко кодировать весь запрос, вы можете сгенерировать оператор sql, используя динамический SQL:

DECLARE @colsUnpivot AS NVARCHAR(MAX),
   @query  AS NVARCHAR(MAX)

select @colsUnpivot 
  = stuff((select ','+quotename(C.column_name)
           from information_schema.columns as C
           where C.table_name = 'yourtable' and
                 C.column_name like 'Indicator%'
           for xml path('')), 1, 1, '')

set @query 
  = 'select id, entityId,
        indicatorname,
        indicatorvalue
     from yourtable
     unpivot
     (
        indicatorvalue
        for indicatorname in ('+ @colsunpivot +')
     ) u'

exec sp_executesql @query;
Тарин
источник
4
Для тех, кто хочет больше гаек и болтов о UNPIVOTи / против. APPLY, это сообщение в блоге Брэда Шульцапоследующие ) 2010 года очень красиво.
ruffin
2
Сообщение 8167, уровень 16, состояние 1, строка 147 Тип столбца «blahblah» конфликтует с типом других столбцов, указанных в списке UNPIVOT.
JDPeckham
@JDPeckham Если у вас разные типы данных, вам необходимо преобразовать их в один и тот же тип и длину, прежде чем выполнять разворачивание. Вот дополнительная информация об этом .
Тарин
у метода xml есть недостаток, поскольку он не может отменить экранирование кодов xml, таких как & gt ;, & lt; и & amp ;. Кроме того, производительность можно значительно улучшить, переписав следующим образом: select @colsUnpivot = stuff ((select ',' + quotename (C.column_name) as [text ()] from information_schema.columns as C, где C.table_name = 'yourtable') и C.column_name, например 'Indicator%' для xml path (''), type) .value ('text () [1]', 'nvarchar (max)'), 1, 1, '')
rrozema
24

ну, если у вас есть 150 столбцов, я думаю, что UNPIVOT не вариант. Итак, вы можете использовать трюк с xml

;with CTE1 as (
    select ID, EntityID, (select t.* for xml raw('row'), type) as Data
    from temp1 as t
), CTE2 as (
    select
         C.id, C.EntityID,
         F.C.value('local-name(.)', 'nvarchar(128)') as IndicatorName,
         F.C.value('.', 'nvarchar(max)') as IndicatorValue
    from CTE1 as c
        outer apply c.Data.nodes('row/@*') as F(C)
)
select * from CTE2 where IndicatorName like 'Indicator%'

sql fiddle demo

Вы также можете написать динамический SQL, но мне больше нравится xml - для динамического SQL у вас должны быть разрешения на выбор данных непосредственно из таблицы, а это не всегда вариант.

ОБНОВЛЕНИЕ
Поскольку в комментариях есть большое пламя, я думаю, что добавлю некоторые плюсы и минусы xml / dynamic SQL. Я постараюсь быть максимально объективным и не упоминать элегантность и некрасивость. Если у вас есть другие плюсы и минусы, отредактируйте ответ или напишите в комментариях

минусы

  • это не так быстро, как динамический SQL, грубые тесты показали, что xml примерно в 2,5 раза медленнее, чем динамический (это был один запрос к таблице ~ 250000 строк, поэтому эта оценка не является точной). Вы можете сравнить это сами, если хотите, вот пример sqlfiddle , на 100000 строк это было 29s (xml) vs 14s (динамическое);
  • может быть, это будет труднее понять людям, не знакомым с xpath;

профи

  • это та же область, что и у других ваших запросов, и это может быть очень удобно. На ум приходят несколько примеров
    • вы можете запрашивать insertedи deletedтаблицы внутри вашего триггера (вообще невозможно с динамическим);
    • пользователю не обязательно иметь права на прямой выбор из таблицы. Я имею в виду, что если у вас есть уровень хранимых процедур и у пользователя есть разрешения на запуск sp, но нет разрешений на запросы к таблицам напрямую, вы все равно можете использовать этот запрос внутри хранимой процедуры;
    • вы можете запросить переменную таблицы, которую вы заполнили в своей области (чтобы передать ее внутри динамического SQL, вам нужно либо сделать ее временной таблицей, либо создать тип и передать его как параметр в динамический SQL;
  • вы можете выполнить этот запрос внутри функции (скалярное или табличное значение). Внутри функций невозможно использовать динамический SQL;
Роман Пекар
источник
2
Какие данные вы выбираете с помощью XML, которые не требуют выбора данных из таблицы?
Аарон Бертран
1
Например, вы можете решить не предоставлять пользователям разрешения на выбор данных из таблиц, а только для хранимых процедур, работающих с таблицами, поэтому я мог бы выбрать xml внутри процедуры, но мне нужно использовать некоторые обходные пути, если я хочу использовать динамический SQL
Роман Пекар
3
Если вы хотите, чтобы ваши пользователи могли выполнять код, вы должны предоставить им любой доступ, который им нужен для выполнения кода. Не придумывайте несуществующих требований, чтобы ваш ответ звучал лучше (вам также не нужно комментировать конкурирующие ответы, чтобы посмотреть на ваш ответ - если они нашли этот ответ, они могут найти и ваш).
Аарон Бертран,
2
Кроме того, если ваше оправдание для использования XML заключается в том, что вы можете поместить его в хранимую процедуру, чтобы избежать прямого доступа к таблице, возможно, ваш пример должен показать, как поместить его в хранимую процедуру и как предоставить права пользователю, чтобы они может выполнить его, не имея доступа на чтение к базовой таблице. Для меня это ползание объема, потому что большинство людей, пишущих запросы к таблице, имеют доступ для чтения к таблице.
Аарон Бертран
2
Я бы сказал, что разница в продолжительности в 10 раз имеет значение, да. И ~ 8000 строк - это не «большие объемы данных» - стоит ли нам посмотреть, что происходит с 800000 строками?
Аарон Бертран
7

Чтобы помочь новым читателям, я создал пример, чтобы лучше понять ответ @ bluefeet о UNPIVOT.

 SELECT id
        ,entityId
        ,indicatorname
        ,indicatorvalue
  FROM (VALUES
        (1, 1, 'Value of Indicator 1 for entity 1', 'Value of Indicator 2 for entity 1', 'Value of Indicator 3 for entity 1'),
        (2, 1, 'Value of Indicator 1 for entity 2', 'Value of Indicator 2 for entity 2', 'Value of Indicator 3 for entity 2'),
        (3, 1, 'Value of Indicator 1 for entity 3', 'Value of Indicator 2 for entity 3', 'Value of Indicator 3 for entity 3'),
        (4, 2, 'Value of Indicator 1 for entity 4', 'Value of Indicator 2 for entity 4', 'Value of Indicator 3 for entity 4')
       ) AS Category(ID, EntityId, Indicator1, Indicator2, Indicator3)
UNPIVOT
(
    indicatorvalue
    FOR indicatorname IN (Indicator1, Indicator2, Indicator3)
) UNPIV;
Dmyan
источник
3

Мне нужно было решение для преобразования столбцов в строки в Microsoft SQL Server, не зная имен столбцов (используемых в триггере) и без динамического sql (динамический sql слишком медленный для использования в триггере).

Я наконец нашел это решение, которое отлично работает:

SELECT
    insRowTbl.PK,
    insRowTbl.Username,
    attr.insRow.value('local-name(.)', 'nvarchar(128)') as FieldName,
    attr.insRow.value('.', 'nvarchar(max)') as FieldValue 
FROM ( Select      
          i.ID as PK,
          i.LastModifiedBy as Username,
          convert(xml, (select i.* for xml raw)) as insRowCol
       FROM inserted as i
     ) as insRowTbl
CROSS APPLY insRowTbl.insRowCol.nodes('/row/@*') as attr(insRow)

Как видите, я конвертирую строку в XML (подзапрос выбирает i, * для xml raw, это преобразует все столбцы в один столбец xml)

Затем я ПЕРЕСЕЧЕНИЕ ПРИМЕНЯЮ функцию к каждому атрибуту XML этого столбца, чтобы получить по одной строке для каждого атрибута.

В целом, это преобразует столбцы в строки, не зная имен столбцов и не используя динамический sql. Это достаточно быстро для моей цели.

(Изменить: я только что видел ответ Романа Пекара выше, который делает то же самое. Сначала я использовал динамический триггер sql с курсорами, который был в 10-100 раз медленнее, чем это решение, но, возможно, это было вызвано курсором, а не динамический sql. В любом случае, это решение очень простое универсальное, так что это определенно вариант).

Я оставляю этот комментарий здесь, потому что хочу сослаться на это объяснение в своем сообщении о триггере полного аудита, которое вы можете найти здесь: https://stackoverflow.com/a/43800286/4160788

паблисити
источник
3
DECLARE @TableName varchar(max)=NULL
SELECT @TableName=COALESCE(@TableName+',','')+t.TABLE_CATALOG+'.'+ t.TABLE_SCHEMA+'.'+o.Name
  FROM sysindexes AS i
  INNER JOIN sysobjects AS o ON i.id = o.id
  INNER JOIN INFORMATION_SCHEMA.TABLES T ON T.TABLE_NAME=o.name
 WHERE i.indid < 2
  AND OBJECTPROPERTY(o.id,'IsMSShipped') = 0
  AND i.rowcnt >350
  AND o.xtype !='TF'
 ORDER BY o.name ASC

 print @tablename

Вы можете получить список таблиц с количеством строк> 350. Вы можете увидеть в списке решений таблицу в виде строки.

cunay
источник
2

Просто потому, что я не видел об этом упоминания.

Если 2016+, вот еще один вариант динамической отмены сводки данных без фактического использования динамического SQL.

пример

Declare @YourTable Table ([ID] varchar(50),[Col1] varchar(50),[Col2] varchar(50))
Insert Into @YourTable Values 
 (1,'A','B')
,(2,'R','C')
,(3,'X','D')

Select A.[ID]
      ,Item  = B.[Key]
      ,Value = B.[Value]
 From  @YourTable A
 Cross Apply ( Select * 
                From  OpenJson((Select A.* For JSON Path,Without_Array_Wrapper )) 
                Where [Key] not in ('ID','Other','Columns','ToExclude')
             ) B

Возвращает

ID  Item    Value
1   Col1    A
1   Col2    B
2   Col1    R
2   Col2    C
3   Col1    X
3   Col2    D
Джон Каппеллетти
источник