Sql Server 2008 - PIVOT без функции агрегирования - PullRequest
6 голосов
/ 21 октября 2011

Я знаю, у вас есть несколько тем, касающихся этого.Но я не нашел тот, который отвечал бы моим потребностям.Мне нужно (по требованию) выбрать глубокие данные таблицы для широкой таблицы вывода.Суть в том, что я не могу использовать агрегат с Pivot, потому что он ест ответы, которые необходимы в выводе.Я разработал решение, но я не думаю, что оно лучшее, потому что для его работы понадобится множество оставшихся соединений.Я включил все попытки и примечания следующим образом:

-- Sql Server 2008 db.
-- Deep table structure (not subject to modification) contains name/value pairs with a userId as
-- foreign key.  In many cases there can be MORE THAN ONE itemValue given by the user for the
-- itemName such as if asked their race, can answer White + Hispanic, etc.  Each response is stored
-- as a seperate record - this cannot currently be changed.

-- Goal: pivot deep data to wide while also compressing result 
-- set down. Account for all items per userId, and duplicating
-- column values (rather than show nulls) as applicable

-- Sample table to store some data of both single and multiple responses
DECLARE @testTable AS TABLE(userId int, itemName varchar(50), itemValue varchar(255))

INSERT INTO @testTable
SELECT 1, 'q01', '1-q01 Answer'
UNION SELECT 1, 'q02', '1-q02 Answer'
UNION SELECT 1, 'q03', '1-q03 Answer 1'
UNION SELECT 1, 'q03', '1-q03 Answer 2'
UNION SELECT 1, 'q03', '1-q03 Answer 3'
UNION SELECT 1, 'q04', '1-q04 Answer'
UNION SELECT 1, 'q05', '1-q05 Answer'
UNION SELECT 2, 'q01', '2-q01 Answer'
UNION SELECT 2, 'q02', '2-q02 Answer'
UNION SELECT 2, 'q03', '2-q03 Answer 1'
UNION SELECT 2, 'q03', '2-q03 Answer 2'
UNION SELECT 2, 'q04', '2-q04 Answer'
UNION SELECT 2, 'q05', '2-q05 Answer'

SELECT 'Raw Data'
SELECT * FROM @TestTable

SELECT 'Using Pivot - shows aggregate result of itemValue per itemName - eats others'
; WITH Data AS (
    SELECT
        [userId]
        , [itemName]
        , [itemValue]
    FROM 
        @testTable
)
SELECT
    [userId]
    , [q02]
    , [q03]
    , [q05]
FROM
    Data
PIVOT
(
    MIN(itemValue)  -- Aggregate function eats needed values.
    FOR itemName in ([q02], [q03], [q05])
) AS PivotTable


SELECT 'Aggregate with Grouping - Causes Null Values'
SELECT
    DISTINCT userId 
    ,[q02] = Max(CASE WHEN itemName = 'q02' THEN itemValue END)
    ,[q03] = Max(CASE WHEN itemName = 'q03' THEN itemValue END)
    ,[q05] = Max(CASE WHEN itemName = 'q05' THEN itemValue END)
FROM
    @testTable
WHERE
    itemName in ('q02', 'q03', 'q05')   -- Makes it a hair quicker
GROUP BY
    userId  -- If by userId only, it only gives 1 row PERIOD = BAD!!
    , [itemName]
    , [itemValue]


SELECT 'Multiple Left Joins - works properly but bad if pivoting 175 columns or so'
; WITH Data AS (
    SELECT
        userId 
        ,[itemName]
        ,[itemValue]
    FROM
        @testTable
    WHERE
        itemName in ('q02', 'q03', 'q05')   -- Makes it a hair quicker
)
SELECT
    DISTINCT s1.userId
    ,[q02] = s2.[itemValue]
    ,[q03] = s3.[itemValue]
    ,[q05] = s5.[itemValue]
FROM
    Data s1
    LEFT JOIN Data s2 
        ON s2.userId = s1.userId 
            AND s2.[itemName] = 'q02'
    LEFT JOIN Data s3 
        ON s3.userId = s1.userId 
            AND s3.[itemName] = 'q03'
    LEFT JOIN Data s5 
        ON s5.userId = s1.userId 
            AND s5.[itemName] = 'q05'

Таким образом, нижний запрос - единственный (пока), который делает то, что мне нужно, но ЛЕВОЕ СОЕДИНЕНИЕ ВЫЙДЕТ из руки вызывает проблемы с производительностью, когда я использую фактические имена элементов для поворота.Любые рекомендации приветствуются.

Ответы [ 4 ]

3 голосов
/ 25 октября 2011
; WITH SRData AS (
    SELECT  -- Only query single response items in this block
        [userId]
        , [q01]
        , [q02]
        , [q04]
        , [q05]
    FROM
        @testTable
    PIVOT
    (
        MIN(itemValue) 
        FOR itemName in ([q01], [q02], [q04], [q05])
    ) AS PivotTable
)
SELECT
    sr.[userId]
    , sr.[q01]
    , sr.[q02]  
    , [q03] = mr03.[itemValue]
    , sr.[q04]      
    , sr.[q05]      
    , [q06] = mr06.[itemValue]
FROM
    SRData sr
    LEFT JOIN @testTable mr03 ON mr03.userId = sr.userId AND mr03.itemName = 'q03'  -- Muli Response for q03
    LEFT JOIN @testTable mr06 ON mr06.userId = sr.userId AND mr06.itemName = 'q06'  -- Muli Response for q06

3 голосов
/ 25 октября 2011

Я думаю, вам придется придерживаться объединений, потому что объединения - это точно способ получения результатов, подобных тому, который вы ищете.Цель объединения состоит в том, чтобы объединить наборы строк вместе (при условии или без таковых), и ваш целевой вывод - не что иное, как комбинация подмножеств строк.

Однако, если большинство вопросов всегда имеютединичные ответы позволяют существенно сократить количество необходимых объединений.Идея состоит в том, чтобы объединять только группы с несколькими ответами в виде отдельных наборов строк.Что касается элементов с одним ответом, они объединяются только как часть всего набора данных целевых элементов.

Пример должен лучше иллюстрировать то, что я мог бы плохо описать в устной форме.Предполагая, что в исходных данных есть две потенциально множественные группы ответа: 'q03' и 'q06' (фактически, вот исходная таблица:

DECLARE @testTable AS TABLE(
  userId int,
  itemName varchar(50),
  itemValue varchar(255)
);

INSERT INTO @testTable
SELECT 1, 'q01', '1-q01 Answer'
UNION SELECT 1, 'q02', '1-q02 Answer'
UNION SELECT 1, 'q03', '1-q03 Answer 1'
UNION SELECT 1, 'q03', '1-q03 Answer 2'
UNION SELECT 1, 'q03', '1-q03 Answer 3'
UNION SELECT 1, 'q04', '1-q04 Answer'
UNION SELECT 1, 'q05', '1-q05 Answer'
UNION SELECT 1, 'q06', '1-q06 Answer 1'
UNION SELECT 1, 'q06', '1-q06 Answer 2'
UNION SELECT 1, 'q06', '1-q06 Answer 3'
UNION SELECT 2, 'q01', '2-q01 Answer'
UNION SELECT 2, 'q02', '2-q02 Answer'
UNION SELECT 2, 'q03', '2-q03 Answer 1'
UNION SELECT 2, 'q03', '2-q03 Answer 2'
UNION SELECT 2, 'q04', '2-q04 Answer'
UNION SELECT 2, 'q05', '2-q05 Answer'
UNION SELECT 2, 'q06', '2-q06 Answer 1'
UNION SELECT 2, 'q06', '2-q06 Answer 2'
;

, которая совпадает с таблицей в исходном сообщении, нос добавлением 'q06' элементов) результирующий скрипт может выглядеть так:

WITH ranked AS (
  SELECT
    *,
    rn = ROW_NUMBER() OVER (PARTITION BY userId, itemName ORDER BY itemValue)
  FROM @testTable
),
multiplied AS (
  SELECT
    r.userId,
    r.itemName,
    r.itemValue,
    rn03 = r03.rn,
    rn06 = r06.rn
  FROM ranked r03
    INNER JOIN ranked r06 ON r03.userId = r06.userId AND r06.itemName = 'q06'
    INNER JOIN ranked r ON r03.userId = r.userId AND (
      r.itemName = 'q03' AND r.rn = r03.rn OR
      r.itemName = 'q06' AND r.rn = r06.rn OR
      r.itemName NOT IN ('q03', 'q06')
    )
  WHERE r03.itemName = 'q03'
    AND r.itemName IN ('q02', 'q03', 'q05', 'q06')
)
SELECT userId, rn03, rn06, q02, q03, q05, q06
FROM multiplied
PIVOT (
  MIN(itemValue)  
  FOR itemName in (q02, q03, q05, q06)
) AS PivotTable
2 голосов
/ 22 октября 2011

Не ясно, как именно должны выглядеть желаемые результаты, но есть одна возможность

; WITH Data AS (
    SELECT
        ROW_NUMBER() OVER (PARTITION BY [userId], [itemName] 
                               ORDER BY [itemValue]) AS RN
        ,  [userId]
        , [itemName]
        , [itemValue]
    FROM 
        @testTable
)
SELECT
    [userId]
    , [q02]
    , [q03]
    , [q05]
FROM
    Data
PIVOT
(
    MIN(itemValue)  
    FOR itemName in ([q02], [q03], [q05])
) AS PivotTable

Возвращает

userId      q02                            q03                            q05
----------- ------------------------------ ------------------------------ ------------------------------
1           1-q02 Answer                   1-q03 Answer 1                 1-q05 Answer
1           NULL                           1-q03 Answer 2                 NULL
1           NULL                           1-q03 Answer 3                 NULL
2           2-q02 Answer                   2-q03 Answer 1                 2-q05 Answer
2           NULL                           2-q03 Answer 2                 NULL
2 голосов
/ 22 октября 2011

Примечание: если у вас есть отдельная таблица с вопросами для каждого пользователя (где userId + itemName - первичный / кандидатный ключ), вы можете удалить первый CTE (UserQuestion) и использовать эту таблицу во втором CTE (UserQuestionWithAllAnswers) вместо пользовательского CTE:

;WITH UserQuestionWithAllAnswers
AS
(
    SELECT   a.userId
            ,a.itemName
            ,ca.AllAnswers
    FROM    TableUserQuestion a ...

Примечание 2. Другим вариантом может быть хранимая процедура CLR.

Производительность не впечатляет, но если вы хотите увидеть все ответы для каждого пользователя и вопроса, тогда этот запрос можетбыть решением:

;WITH UserQuestion AS 
(
    SELECT   x.userId
            ,x.itemName
    FROM    @testTable x
    GROUP BY x.userId, x.itemName
), UserQuestionWithAllAnswers
AS
(
    SELECT   a.userId
            ,a.itemName
            ,ca.AllAnswers
    FROM    UserQuestion a
    CROSS APPLY
    (
        SELECT SUBSTRING(
            (SELECT ','+b.itemValue
            FROM    @testTable b
            WHERE   a.userId = b.userId 
            AND     a.itemName = b.itemName 
            FOR XML PATH(''))
            ,2
            ,8000) AS AllAnswers
    ) ca
)
SELECT  pvt.*
FROM    UserQuestionWithAllAnswers src
PIVOT
( MIN(src.AllAnswers) FOR itemName IN ([q01], [q02], [q03], [q04], [q05]) ) AS pvt;

Результаты:

userId q01          q02          q03                                          q04          q05          
------ ------------ ------------ -------------------------------------------- ------------ -------------
1      1-q01 Answer 1-q02 Answer 1-q03 Answer 1,1-q03 Answer 2,1-q03 Answer 3 1-q04 Answer 1-q05 Answer
2      2-q01 Answer 2-q02 Answer 2-q03 Answer 1,2-q03 Answer 2                2-q04 Answer 2-q05 Answer
...