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

Плохое название для поста, но, надеюсь, оно привлечет внимание.

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


ПРЕДИСЛОВИЕ

У меня есть таблица Customers с уникальным идентификатором. У этих клиентов есть файлы, у файлов есть свойства, и каждому свойству Имя соответствует одно значение.

Таблицы:

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

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


ПРИМЕР ИСПОЛЬЗОВАНИЯ

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

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


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

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


ДО ПОВОРОТА

| 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           |

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


ПОСЛЕ РАЗВИТИЯ (ожидание)

| 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

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

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

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


person BlueCucumber    schedule 09.08.2016    source источник
comment
Хм, вы пытались сократить набор данных только до соответствующих строк, выбрав только самый последний (самый высокий UpdatedDtm) для каждой комбинации файл/свойство перед выполнением PIVOT? Таким образом, PIVOT уже сможет дать вам тот результат, который вы ищете, не так ли? Извините, я не могу проверить TSQL в данный момент...   -  person hsan    schedule 09.08.2016
comment
Да, я буду разбивать данные на блоки, например, 1 час назад, 2 часа и т. д., но вариант использования сосредоточен вокруг выбора первого ненулевого значения для каждого столбца и сохранения всех столбцов, как при перекрестном соединении, позволяющем нули.   -  person BlueCucumber    schedule 10.08.2016


Ответы (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
person Matt    schedule 09.08.2016
comment
Я новичок в CTE, но я попробовал ваш запрос, и он продолжает говорить мне Msg 207, уровень 16, состояние 1, строка 2 Недопустимое имя столбца «Свойство». Любые идеи? - person BlueCucumber; 10.08.2016
comment
В вашей документации свойство указано как имя столбца, или я ошибаюсь? Ошибка означает, что он не может найти столбец с таким именем в вашей таблице. Также, возможно, вы где-то ошиблись, если пытались объединить то, что я написал, с вашими запросами? - person Matt; 10.08.2016
comment
Я дважды проверил и проверил еще раз, возможно, мне нужно было закрыть сеанс и начать новый запрос. Мой кеш мог сохранить неверное определение таблицы. РЕДАКТИРОВАТЬ: Только что попробовал, та же сделка. Временная таблица, из которой я извлекаю, имеет правильно названный столбец в Select, поэтому я понятия не имею, почему dynSql не может использовать псевдоним. - person BlueCucumber; 10.08.2016
comment
ctrl + shift + r обновит ваше автозаполнение / intellisense - person Matt; 10.08.2016
comment
Вложенный внутренний выбор 'r' работает. Подзапрос 't' работает. Поэтому я подумал про себя, единственное, что осталось, это «Заказать», и по какой-то причине у меня случайно есть «Свойство» вместо «Имя файла» в качестве моего заказа. Задача решена. Спасибо! - person BlueCucumber; 10.08.2016
comment
очень рад, что это сработало для вас. Пожалуйста, примите мой ответ, если вы продвигаетесь вперед, спасибо! meta.stackexchange.com/questions/ 5234/ - person Matt; 10.08.2016

Возможно, вам потребуется добавить условие 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
person DVT    schedule 09.08.2016
comment
Я не могу получить оператор where в CTE, чтобы захватить диапазон дат и взять самое молодое или самое старое первое ненулевое значение внутри них. Это быстрое и грязное решение, которое опирается на Max() для результата, и я хотел бы уйти от этого. - person BlueCucumber; 10.08.2016
comment
Оператор Max() предназначен для поворота, а не для фильтрации результата. Pivot всегда требует функции агрегирования, и в этом случае Max() является очевидным выбором. Главное, вам нужно убедиться, что из CTE возвращается уникальная комбинация ‹CustomerID, FileName, Property›. - person DVT; 10.08.2016