ТОП (1) ПО ГРУППЕ очень огромного (100 000 000+) столов

8

Настроить

У меня есть огромная таблица ~ 115 382 254 строк. Таблица является относительно простой и регистрирует операции процесса приложения.

CREATE TABLE [data].[OperationData](
    [SourceDeciveID] [bigint] NOT NULL,
    [FileSource] [nvarchar](256) NOT NULL,
    [Size] [bigint] NULL,
    [Begin] [datetime2](7) NULL,
    [End] [datetime2](7) NOT NULL,
    [Date]  AS (isnull(CONVERT([date],[End]),CONVERT([date],'19000101',(112)))) PERSISTED NOT NULL,
    [DataSetCount] [bigint] NULL,
    [Result] [int] NULL,
    [Error] [nvarchar](max) NULL,
    [Status] [int] NULL,
 CONSTRAINT [PK_OperationData] PRIMARY KEY CLUSTERED 
(
    [SourceDeviceID] ASC,
    [FileSource] ASC,
    [End] ASC
))

CREATE TABLE [model].[SourceDevice](
    [ID] [bigint] IDENTITY(1,1) NOT NULL,
    [Name] [nvarchar](50) NULL,
 CONSTRAINT [PK_DataLogger] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
))

ALTER TABLE [data].[OperationData]  WITH CHECK ADD  CONSTRAINT [FK_OperationData_SourceDevice] FOREIGN KEY([SourceDeviceID])
REFERENCES [model].[SourceDevice] ([ID])

Таблица состоит из 500 кластеров и ежедневно.

перегородки

введите описание изображения здесь

Кроме того, таблица хорошо индексируется по PK, статистика актуальна, и INDEXer подвергается дефрагментации каждую ночь.

SELECT на основе индекса работают молниеносно, и у нас не было с этим проблем.

проблема

Мне нужно знать последнюю (TOP) строку [End]и разделить на [SourceDeciveID]. Чтобы получить самый последний [OperationData]из каждого исходного устройства.

Вопрос

Мне нужно найти способ решить эту проблему хорошим способом, не доводя БД до предела.


Усилие 1

Первая попытка была очевидна GROUP BYили SELECT OVER PARTITION BYзапрос. Проблема здесь также очевидна, каждый запрос должен сканировать по порядку самого раздела / находить верхнюю строку. Таким образом, запрос очень медленный и имеет очень большое влияние ввода-вывода.

Пример запроса 1

;WITH cte AS
(
   SELECT *,
         ROW_NUMBER() OVER (PARTITION BY [SourceDeciveID] ORDER BY [End] DESC) AS rn
   FROM [data].[OperationData]
)
SELECT *
FROM cte
WHERE rn = 1

Пример запроса 2

SELECT *
FROM [data].[OperationData] AS d 
CROSS APPLY 
(
   SELECT TOP 1 *
   FROM [data].[OperationData] 
   WHERE [SourceDeciveID] = d.[SourceDeciveID]
   ORDER BY [End] DESC
) AS ds

НЕ СМОГЛИ!

Усилие 2

Я создал справочную таблицу, чтобы всегда содержать ссылку на строку TOP.

CREATE TABLE [data].[LastOperationData](
    [SourceDeciveID] [bigint] NOT NULL,
    [FileSource] [nvarchar](256) NOT NULL,
    [End] [datetime2](7) NOT NULL,
 CONSTRAINT [PK_LastOperationData] PRIMARY KEY CLUSTERED 
(
    [SourceDeciveID] ASC
)

ALTER TABLE [data].[LastOperationData]  WITH CHECK ADD  CONSTRAINT [FK_LastOperationData_OperationData] FOREIGN KEY([SourceDeciveID], [FileSource], [End])
REFERENCES [data].[OperationData] ([SourceDeciveID], [FileSource], [End])

Для заполнения таблицы создан триггер, который всегда добавляет / обновляет исходную строку, если [End]вставлен столбец более высокого уровня .

CREATE TRIGGER [data].[OperationData_Last]
   ON  [data].[OperationData]
   AFTER INSERT
AS 
BEGIN
    SET NOCOUNT ON;

    MERGE [data].[LastOperationData] AS [target]
    USING (SELECT [SourceDeciveID], [FileSource], [End] FROM inserted) AS [source] ([SourceDeciveID], [FileSource], [End])  
    ON ([target].[SourceDeciveID] = [FileSource].[SourceDeciveID])

    WHEN MATCHED AND [target].[End] < [source].[End] THEN
        UPDATE SET [target].[FileSource] = source.[FileSource], [target].[End] = source.[End]

    WHEN NOT MATCHED THEN  
        INSERT ([SourceDeciveID], [FileSource], [End])  
        VALUES (source.[SourceDeciveID], source.[FileSource], source.[End]);

END

Проблема здесь в том, что он также оказывает очень большое влияние на IO, и я не знаю почему.

Как вы можете видеть здесь в плане запроса, он также выполняет сканирование всей [OperationData]таблицы.

Это имеет огромное общее влияние на мою БД. статистика

НЕ СМОГЛИ!

Штеффен Мангольд
источник
2
В вашем первом кодовом блоке я не вижу, откуда берется первый столбец кластерного индекса - правильно?
Джордж. Паласиос
Да, извините, SSMS не включает его в CREATE TABLEскрипт, но внутри плана запроса вы увидите разделы. Я отредактирую вопрос.
Штеффен Мангольд
Не лишний индекс, потому что, как PRIMARY KEY CLUSTEREDвы думаете, он может помочь?
Штеффен Мангольд
Сорый, это было ошибкой, я изменил названия вопроса на более понятные, я исправил их.
Штеффен Мангольд
@ ypercubeᵀᴹ да, потому что SELECT [SourceID], [Source], [End] FROM insertedнекоторые, как сделать сканирование таблицы на [OperationData].
Штеффен Мангольд

Ответы:

9

Если у вас есть таблица SourceIDзначений и индекс главной таблицы (SourceID, End) include (othercolumns), просто используйте OUTER APPLY.

SELECT d.*
FROM dbo.Sources s
OUTER APPLY (SELECT TOP (1) *
    FROM data.OperationData d
    WHERE d.SourceID = s.SourceID
    ORDER BY d.[End] DESC) d;

Если вы знаете, что вы только после вашего нового раздела, вы можете включить фильтр на конец, как AND d.[End] > DATEADD(day, -1, GETDATE())

Изменить: так как ваш кластерный индекс SourceID, Source, End)включен, поместите источник в свою таблицу источников и присоединиться к нему также. Тогда вам не нужен новый индекс.

SELECT d.*
FROM dbo.Sources s -- Small table
OUTER APPLY (SELECT TOP (1) *
    FROM data.OperationData d -- Big table quick seeks
    WHERE d.SourceID = s.SourceID
    AND d.Source = s.Source
    AND d.[End] > DATEADD(day, -1, GETDATE()) -- If you’re partitioning on [End], do this for partition elimination
    ORDER BY d.[End] DESC) d;
Роб Фарли
источник
Индекс действительно ускорил запрос. Вторая проблема, которая возникает, заключается в том, что неразделенный индекс в такой огромной таблице практически не поддерживается. На всей нашей таблице «больших данных» мы работаем с секционированным индексатором. Их можно поддерживать онлайн раздел за разделом. Как только индексатор секционируется, проблема становится старой, потому что он должен проходить через каждый раздел.
Штеффен Мангольд
1
@SteffenMangold: чем меньше данных в индексе, тем лучше (если в нем есть все, что вам нужно), и, за исключением материализованных представлений, кластеризованный индекс содержит максимально возможный объем данных. Кластерные индексы присутствуют, потому что получение всех данных по ключу является нормой. В этом случае вы получаете все данные, но на самом деле вы не получаете их по ключу, вы получаете их по части ключа. Вам нужен индекс, который можно запросить с помощью части ключа.
Jmoreno
Мне очень жаль, но есть Sourceтаблица, ссылающаяся на sourceIDколонку. Источник столбца - это только имя файла. Это немного сбивает с толку имен. Для каждого Sourceустройства (sourceID) может быть только одна запись для одного файла source(столбца) в одной временной отметке. Также я не могу удалить разделы, потому что самые новые Endфрагментированы. Вот почему я придумал триггерное решение. Я думаю, что живой запрос здесь не будет работать.
Штеффен Мангольд
@Rob Farley Я отредактировал вопрос, чтобы быть более ясным
Штеффен Мангольд
С разделением вы обнаружите, что он выполняет все эти поиски в каждом разделе. С дополнительным предикатом вы можете сделать так, чтобы он не беспокоил их всех, а делал только некоторые. Сделайте это месяц, если вам нужно.
Роб Фарли