Как развернуть данные sql и объединить результаты в ненулевые строки по дате на идентификатор

Неплохой заголовок для поста, но, надеюсь, он заинтересует.

У меня очень сложная ситуация в T-SQL, которую я не могу решить. Я надеюсь, что кто-то с опытом знает элегантное и быстрое решение, чтобы не повлиять на мою производительность. Я имею дело с миллиардами строк.


ПРЕДИСЛОВИЕ

У меня есть таблица с названием " Клиенты" с уникальным идентификатором. Эти клиенты имеют файлы, файлы имеют свойства, и каждое имя свойства соответствует одному значению.

Таблицы:

  • Клиенты
  • Файлы -
  • Свойство - содержит как имя, так и значение

Идентификатор клиента присутствует во всех этих таблицах, как и поля аудита, такие как UpdatedDtm и CreationDtm.


Случай использования

Мне нужно присоединить всех клиентов к своим файлам (фильтрация для нескольких), а затем привязать каждый файл к их свойствам (снова фильтруя их). Это легко, но в результате получается много строк, по одной для каждого клиента x file x property.

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


ПРОБЛЕМА Во-первых, свойства имеют DateTime, когда они были изменены (updatedDtm), и мне нужно вернуть все измененное с 1 часа от даты создания (CreationDtm) в таблице файлов.

Это приводит к тому, что я урезаю свой список потенциальных свойств, но теперь у меня есть таблица с RowNumber() для идентификатора и нет хорошего способа развернуть и выбрать первый, который не равен NULL и при этом сохраняется количество столбцов для таблица определений. Это важно, потому что я использую Dynamic SQL и помещаю его в индексированную временную таблицу с составным ключом для CustomerID и FileName.


ДО PIVOT

| UpdatedDtm | CustomerID | FileName   | Property | Value          |
| ---------- | ---------- | ---------- | -------- | -------------- |
| 1/1/2015   | 1          | FileOne    | Size     | NULL           |
| 1/1/2015   | 1          | FileOne    | Format   | JPG            |
| 1/7/2015   | 1          | FileOne    | Size     | 88KB           |
| 1/7/2015   | 1          | FileOne    | Format   | JPG            |
| 1/7/2015   | 1          | FileOne    | Comment  | NULL           |
| 1/11/2015  | 1          | FileOne    | Comment  | NULL           |
| 1/1/2015   | 1          | FileTwo    | Size     | 91KB           |
| 1/1/2015   | 1          | FileTwo    | Format   | PNG            |
| 1/11/2015  | 1          | FileTwo    | Comment  | NULL           |
| 1/2/2015   | 2          | FileThree  | Size     | 74KB           |
| 1/2/2015   | 2          | FileThree  | Format   | XLS            |
| 1/2/2015   | 2          | FileThree  | State    | Open           |
| 1/7/2015   | 2          | FileThree  | State    | Closed         |
| 1/10/2015  | 2          | FileThree  | Comment  | NULL           |
| 1/1/2015   | 3          | FileFour   | Size     | 2KB            |
| 1/2/2015   | 3          | FileFour   | Size     | 10KB           |
| 1/3/2015   | 3          | FileFour   | Size     | 13KB           |
| 1/4/2015   | 3          | FileFour   | Size     | 21KB           |
| 1/5/2015   | 3          | FileFour   | Size     | 27KB           |
| 1/6/2015   | 3          | FileFour   | Size     | 32KB           |
| 1/7/2015   | 3          | FileFour   | Size     | 39KB           |
| 1/8/2015   | 3          | FileFour   | Size     | 44KB           |
| 1/1/2015   | 3          | FileFour   | Format   | TXT            |
| 1/1/2015   | 3          | FileFour   | Comment  | NULL           |

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


ПОСЛЕ PIVOT (Ожидание)

| CustomerID | FileName   | Size | Format | State  | Comment |
| ---------- | ---------- | ---- | ------ | ------ | ------- |
| 1          | FileOne    | 88KB | JPG    | NULL   | NULL    |
| 1          | FileTwo    | 91KB | PNG    | NULL   | NULL    |
| 2          | FileThree  | 74KB | XLS    | Closed | NULL    |
| 3          | FileFour   | 44KB | TXT    | NULL   | NULL    |

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


КОД (Моя попытка)

IF Object_id('tempdb..#FilesQuery') IS NOT NULL DROP TABLE #FilesQuery;
CREATE TABLE #FilesQuery (
    SeqNum          int,
    CustomerID      numeric(16,0),
    FileName        varchar(64),
    PropertyName    varchar(64),
    PropertyValue   varchar(64)
)
INSERT INTO #FilesQuery
SELECT
     CASE WHEN P.[Value] IS NOT NULL
          THEN ROW_NUMBER() OVER (partition by C.CustomerID order by UpdatedDtm)
          ELSE 0
     END as SeqNum,
     C.CustomerID
    ,F.Name  as FileName
    ,P.Name  as PropertyName
    ,P.Value as PropertyValue

FROM Customers C
INNER JOIN Files F ON F.CustomerID = C.CustomerID
LEFT JOIN Properties P
    ON P.CustomerID = C.CustomerID
    AND P.FileID = F.FileID

WHERE F.FileName IN ('FileOne','FileTwo','FileThree','FileFour')
    AND P.Name IN ('Size','Format','State','Comment')

--PIVOT
DECLARE @cols AS nvarchar(MAX)
SELECT @cols = STUFF(
    (SELECT DISTINCT ',' + QUOTENAME(PropertyName)
       FROM #FilesQuery fq
        FOR XML PATH(''),TYPE).value('.','NVARCHAR(MAX)'),1,1,'')

DECLARE @dynSql AS nvarchar(MAX)
SET @dynSql = '
    SELECT DISTINCT *
    FROM (
        SELECT
            fq.CustomerID,
            fq.FileName,
            fq.PropertyName,
            fq.PropertyValue
        FROM #FilesQuery fq
    ) SRC
    PIVOT (
        Max([PropertyValue])
        FOR PropertyName IN (' + @cols + ')
    ) PVT
'

IF Object_id('tempdb..#Results') IS NOT NULL DROP TABLE #Results;
CREATE TABLE #Results (
    CustomerID      varchar(16) NOT NULL,
    FileName        varchar(64) NOT NULL,
    FileSize        varchar(64) NULL,
    FileFormat      varchar(64) NULL,
    FileState       varchar(64) NULL,
    FileComment     varchar(64) NULL,
    CONSTRAINT pk_CustDoc PRIMARY KEY (CustomerID,FileName)
)
INSERT INTO #Results EXEC @dynSql;

Извините, этот код не полный, это рабочий раздел, который у меня есть. Другие попытки привели к сбоям данных.

Я попытался использовать SeqNum и комбинацию операторов case, чтобы попытаться выбрать первое ненулевое значение для каждой строки, чтобы все данные были в одной строке, но в итоге получилось больше.

FileOne NULL NULL Open NULL
FileOne NULL JPG  NULL NULL

и так далее...

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

У кого-нибудь есть хорошее решение? Я обдумывал вещи?

2 ответа

Решение

Вы должны отфильтровать свои данные перед вами PIVOT и вы получите желаемый результат. Вот версия cte, чтобы показать вам, как получить то, что вы хотите.

;WITH cteDefineRowPrecedence AS (
    SELECT *
       ,ROW_NUMBER() OVER (PARTITION BY CustomerId, FileName, Property ORDER BY
          CASE WHEN Value IS NOT NULL THEN 0 ELSE 1 END
          ,UpdatedDtm DESC) as RowNum
    FROM
       @Table
)

, cteDesiredRwows AS (
    SELECT
       CustomerId
       ,FileName
       ,Property
       ,Value
    FROM
       cteDefineRowPrecedence t
    WHERE
       t.RowNum = 1
       AND t.Value IS NOT NULL
)

SELECT *
FROM
    cteDesiredRwows t
    PIVOT (
       MAX(Value)
       FOR Property IN (Size,[Format],[State],Comment)
    ) p
ORDER BY
    CustomerId
    ,FileName

А вот версия с вложенным запросом, которая облегчит встраивание / вставку в ваш динамический sql....

SELECT *
FROM
    (
       SELECT CustomerId, FileName, Property, Value
       FROM
          (SELECT *
             ,ROW_NUMBER() OVER (PARTITION BY CustomerId, FileName, Property ORDER BY
                CASE WHEN Value IS NOT NULL THEN 0 ELSE 1 END
                ,UpdatedDtm DESC) as RowNum
          FROM
             @Table) r
       WHERE
          r.RowNum = 1
          AND r.Value IS NOT NULL
    ) t
    PIVOT (
       MAX(Value)
       FOR Property IN (Size,[Format],[State],Comment)
    ) p
ORDER BY
    CustomerId
    ,FileName

Возможно, вам понадобится добавить условие WHERE в определение CTE, чтобы ограничить диапазон дат / времени тем, что вы хотите.

   WITH CTE AS (
    SELECT DISTINCT
        CustomerID
        , FileName
        , Property
        , Value
    FROM
        <table_name>
    )
    SELECT *
    FROM
        CTE
        PIVOT (MAX(value) FOR Property IN( 'Size', 'Format', 'State', 'Comment')) p
Другие вопросы по тегам