Как я могу эффективно вычислить MAX одного столбца, упорядоченного по другому столбцу? - PullRequest
2 голосов
/ 25 июня 2010

У меня есть схема таблицы, аналогичная следующей (упрощенной):

CREATE TABLE Transactions
(
    TransactionID int NOT NULL IDENTITY(1, 1) PRIMARY KEY CLUSTERED,
    CustomerID int NOT NULL,  -- Foreign key, not shown
    TransactionDate datetime NOT NULL,
    ...
)

CREATE INDEX IX_Transactions_Customer_Date
ON Transactions (CustomerID, TransactionDate)

Чтобы немного рассказать об этом, эта таблица транзакций фактически объединяет несколько различных типов транзакций из базы данных другого поставщика (мы назовем это процессом ETL), и поэтому я не имею большого контроля над порядок, в котором они вставляются. Даже если бы я это сделал, транзакции могут иметь заднюю дату, поэтому важно отметить, что максимум TransactionID для любой данной customer не обязательно является самой последней транзакцией.

На самом деле самая последняя транзакция представляет собой комбинацию даты и идентификатора. Даты не уникальны - поставщик часто усекает время дня - поэтому, чтобы получить самую последнюю транзакцию, я должен сначала найти самую последнюю дату, а затем найти самый последний идентификатор для этой даты.

Я знаю, что могу сделать это с помощью оконного запроса (ROW_NUMBER() OVER (PARTITION BY TransactionDate DESC, TransactionID DESC)), но для этого требуется полное сканирование индекса и очень дорогая сортировка, и, таким образом, он терпит неудачу с точки зрения эффективности. Также довольно неловко продолжать писать все время.

Немного эффективнее использовать два CTE или вложенных подзапроса, один для поиска MAX(TransactionDate) на CustomerID, а другой для поиска MAX(TransactionID). Опять же, это работает, но требует второго агрегата и объединения, что немного лучше, чем запрос ROW_NUMBER(), но все же довольно болезненно с точки зрения производительности.

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

Итак, вопрос, в частности:

Можно ли написать запрос, который будет возвращать новейший TransactionID за CustomerID, определенный как максимальный TransactionID для самого последнего TransactionDate, и достичь План эквивалентен по производительности обычному MAX / GROUP BY запросу?

(Другими словами, единственными значительными шагами в плане должны быть сканирование индекса и объединение потоков. Многократные сканирования, сортировки, объединения и т. Д., Вероятно, будут слишком медленными.)

Ответы [ 5 ]

1 голос
/ 25 июня 2010

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

Select T.CustomerId, T.TransactionDate, Max(TransactionId)
From Transactions As T
    Join    (
            Select T1.CustomerID, Max(T1.TransactionDate) As MaxDate
            From Transactions As T1
            Group By T1.CustomerId
            ) As Z
        On Z.CustomerId = T.CustomerId
            And Z.MaxDate = T.TransactionDate
Group By T.CustomerId, T.TransactionDate
1 голос
/ 25 июня 2010

Самый полезный индекс может быть:

CustomerID, TransactionDate desc, TransactionId desc

Тогда вы можете попробовать запрос, подобный этому:

select  a.CustomerID
,       b.TransactionID
from    (
        select  distinct
                CustomerID
        from    YourTable
        ) a
cross apply   
        (
        select  top 1
                TransactionID
        from    YourTable
        where   CustomerID = a.CustomerID
        order by
                TransactionDate desc,
                TransactionId desc
        ) b
0 голосов
/ 25 июня 2010

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

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

Это выглядит немного нечестивым, но, похоже, это помогает:

SELECT
    CustomerID,
    CAST(SUBSTRING(MAX(
        CAST(TransactionDate AS binary(8)) + 
        CAST(TransactionID AS binary(4))),
      9, 4) AS int) AS TransactionID
FROM Transactions
GROUP BY CustomerID

Это дает мне одно сканирование индекса и объединение потоков.Никаких дополнительных индексов также не требуется, он выполняет те же действия, что и просто MAX(TransactionID), что, очевидно, имеет смысл, поскольку вся конкатенация происходит внутри самого агрегата.

0 голосов
/ 25 июня 2010

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

SELECT
    T1.customer_id,
    MAX(T1.transaction_id) AS transaction_id
FROM
    dbo.Transactions T1
INNER JOIN
(
    SELECT
        T2.customer_id,
        MAX(T2.transaction_date) AS max_dt
    FROM
        dbo.Transactions T2
    GROUP BY
        T2.customer_id
) SQ1 ON
    SQ1.customer_id = T1.customer_id AND
    T1.transaction_date = SQ1.max_dt
GROUP BY
    T1.customer_id
0 голосов
/ 25 июня 2010

Отказ от ответственности: Мысли вслух:)

Не могли бы вы иметь индексированный вычисляемый столбец, который объединяет столбцы TransactionDate и TransactionID в форму, которая означает, что поиск самой последней транзакции - это всего лишь случай обнаружения МАКСА этого единственного поля?

...