TOPO (1) POR GRUPO de mesa muito grande (mais de 100.000.000)

8

Configuração

Eu tenho uma tabela enorme de ~ 115.382.254 linhas. A tabela é relativamente simples e registra as operações do processo de aplicativo.

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

A tabela está agrupada em cerca de 500 clusters e diariamente.

partições

insira a descrição da imagem aqui

Além disso, a tabela é bem indexada pelo PK, as estatísticas estão atualizadas e o INDEXer é exibido diariamente todas as noites.

Os SELECTs baseados em índices são extremamente rápidos e não tivemos nenhum problema com isso.

Problema

Preciso conhecer a última linha (TOP) [End]e particionada por [SourceDeciveID]. Para obter o último [OperationData]de todos os dispositivos de origem.

Questão

Preciso encontrar uma maneira de resolver isso de uma maneira boa e sem levar o DB ao limite.


Esforço 1

A primeira tentativa foi óbvia GROUP BYou SELECT OVER PARTITION BYconsulta. O problema aqui também é óbvio: todas as consultas precisam varrer muito a ordem das partições / encontrar a linha superior. Portanto, a consulta é muito lenta e tem um impacto de IO muito alto.

Consulta de exemplo 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

Consulta de exemplo 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

FALHOU!

Esforço 2

Criei uma tabela de ajuda para manter sempre uma referência à linha 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])

Para preencher a tabela, criou um gatilho para sempre adicionar / atualizar a linha de origem se uma [End]coluna superior for inserida.

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

O problema aqui é que ele também tem um enorme impacto de IO e não sei por que.

Como você pode ver aqui no plano de consulta, ele também executa uma verificação em toda a [OperationData]tabela.

Ele tem um enorme impacto geral no meu banco de dados. Estatísticas

FALHOU!

Steffen Mangold
fonte
2
No seu primeiro bloco de código, não consigo ver de onde vem a primeira coluna do índice em cluster - está certo?
George.Palacios
Sim, desculpe, o SSMS não o inclui no CREATE TABLEscript, mas dentro do plano de consulta você verá as partições. Vou editar a pergunta.
Steffen Mangold
Não é um índice extra, porque incluído dentro do que PRIMARY KEY CLUSTEREDvocê acha que pode ajudar?
Steffen Mangold
Embora tenha sido um erro, modifiquei os nomes da pergunta para que ficasse mais claro, corrigi-a.
Steffen Mangold 28/11
@ ypercubeᵀᴹ sim, porque SELECT [SourceID], [Source], [End] FROM insertedalguns de como fazer uma varredura de tabela no [OperationData].
Steffen Mangold

Respostas:

9

Se você possui uma tabela de SourceIDvalores e um índice em sua tabela principal (SourceID, End) include (othercolumns), basta usar 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;

Se você souber que está apenas após a sua partição mais recente, poderá incluir um filtro no End, como AND d.[End] > DATEADD(day, -1, GETDATE())

Editar: como o índice em cluster está ativado SourceID, Source, End), coloque o código-fonte na tabela de fontes também e participe também. Então você não precisa do novo índice.

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;
Rob Farley
fonte
O índice realmente acelerou a consulta. Um segundo problema que surge é que um índice não particionado em uma tabela tão grande é quase impossível de manter. Em toda a nossa tabela de "big data", trabalhamos com o indexador particionado. Eles podem ser mantidos partição online por partição. Assim que o indexador é particionado, o problema é antigo, porque ele precisa executar todas as partições.
Steffen Mangold
11
@SteffenMangold: quanto menos dados em um índice, melhor (desde que ele tenha tudo o que você precisa) e excluindo visualizações materializadas, o índice clusterizado tem a quantidade máxima de dados possível. Os índices agrupados estão presentes porque obter todos os dados pela chave é a norma. Nesse caso, você está obtendo todos os dados, mas na verdade não está obtendo os dados pela chave, está obtendo por parte da chave. Você precisa de um índice que possa ser consultado com parte da chave.
jmoreno
Sinto muito, mas há uma Sourcetabela referenciando a sourceIDcoluna. A fonte da coluna é apenas um nome de arquivo. É um nome um pouco confuso. Para cada Sourcedispositivo (sourceID), pode haver apenas uma única entrada para um arquivo source(coluna) em um registro de data e hora. Também não posso eliminar a partição porque o mais novo Endé amplamente fragmentado. É por isso que eu vim com a solução de gatilho. Acho que uma consulta ao vivo não funcionará aqui.
Steffen Mangold
@ Rob Farley Eu editei a pergunta a ser mais claro
Steffen Mangold
Com o particionamento, você encontrará todas as buscas em cada partição. Com o predicado extra, você pode fazê-lo para que não se incomode com todos eles, e apenas com alguns. Faça um mês, se precisar.
Rob Farley