TOP (1) PER GRUPPO di un enorme tavolo (100.000.000+)


8

Impostare

Ho un enorme tavolo di ~ 115.382.254 righe. La tabella è relativamente semplice e registra le operazioni del processo dell'applicazione.

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

La tabella è raggruppata in circa 500 cluster e su base giornaliera.

partizioni

inserisci qui la descrizione dell'immagine

Inoltre, la tabella è ben indicizzata da PK, le statistiche sono aggiornate e l'INDEXer viene deframmentato ogni notte.

I SELECT basati sull'indice sono velocissimi e non abbiamo avuto alcun problema.

Problema

Devo conoscere l'ultima riga (TOP) di [End]e partizionata da [SourceDeciveID]. Per ottenere l'ultimo [OperationData]di ogni dispositivo sorgente.

Domanda

Devo trovare un modo per risolverlo in modo positivo e senza portare il DB al limite.


Sforzo 1

Il primo tentativo è stato ovvio GROUP BYo SELECT OVER PARTITION BYinterrogativo. Anche qui il problema è evidente, ogni query deve eseguire la scansione su un ordine di partizione / trovare la riga superiore. Quindi la query è molto lenta e ha un impatto IO molto elevato.

Query di esempio 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

Esempio di query 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

FALLITA!

Sforzo 2

Ho creato una tabella di aiuto per contenere sempre un riferimento alla riga 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])

Per riempire la tabella è stato creato un trigger per aggiungere / aggiornare sempre la riga di origine se [End]è inserita una colonna superiore .

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

Il problema qui è che ha anche un impatto IO enorme e non so perché.

Come puoi vedere qui nel piano delle query , esegue anche una scansione sull'intera [OperationData]tabella.

Ha un enorme impatto complessivo sul mio DB. statistiche

FALLITA!


2
Nel tuo primo blocco di codice non riesco a vedere da dove provenga la prima colonna dell'indice cluster - giusto?
George.Palacios,

Sì, scusa SSMS non lo include nello CREATE TABLEscript ma all'interno del piano di query vedrai le partizioni. Modificherò la domanda.
Steffen Mangold,

Non un indice aggiuntivo perché incluso all'interno di PRIMARY KEY CLUSTEREDte pensi che possa aiutare?
Steffen Mangold,

Soryy è stato un errore, ho modificato i nomi della domanda in modo più chiaro, l'ho corretto.
Steffen Mangold,

@ ypercubeᵀᴹ sì perché SELECT [SourceID], [Source], [End] FROM insertedalcuni come eseguono la scansione di una tabella su [OperationData].
Steffen Mangold,

Risposte:


9

Se hai una tabella di SourceIDvalori e un indice sulla tabella principale attiva (SourceID, End) include (othercolumns), basta usare 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 sai di essere solo dopo la tua ultima partizione, potresti includere un filtro su Fine, ad esempio AND d.[End] > DATEADD(day, -1, GETDATE())

Modifica: poiché l'indice cluster è attivo SourceID, Source, End), inserisci anche Source nella tabella Sources e unisciti anche a quello. Quindi non è necessario il nuovo indice.

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;

L'indice ha davvero accelerato la query. Un secondo problema che ne deriva è che un indice non partizionato su una tabella così grande è quasi impossibile da mantenere. Su tutta la nostra tabella "big-data" lavoriamo con l'indicizzatore partizionato. Possono essere gestiti partizione online per partizione. Non appena l'indicizzatore viene partizionato, il problema è quello vecchio perché deve eseguire tutte le partizioni.
Steffen Mangold,

1
@SteffenMangold: minore è il numero di dati in un indice, meglio è (purché abbia tutto il necessario) ed escludendo le viste materializzate, l'indice cluster ha la massima quantità possibile di dati. Gli indici cluster sono presenti perché ottenere tutti i dati dalla chiave è la norma. In questo caso stai ricevendo tutti i dati, ma non li stai davvero ottenendo con la chiave, ma li stai ricevendo da parte della chiave. È necessario un indice su cui è possibile eseguire query con parte della chiave.
jmoreno,

Mi dispiace davvero ma c'è una Sourcetabella che fa riferimento alla sourceIDcolonna. L'origine della colonna è solo un nome file. È un nome un po 'confuso. Per ogni Sourcedispositivo (sourceID) potrebbe esserci una sola voce per un file source(colonna) in un timestamp. Inoltre non posso eliminare la partizione perché la più recente Endè ampiamente frammentata. Ecco perché mi è venuta in mente la soluzione trigger. Penso che una query live non funzionerà qui.
Steffen Mangold,

@Rob Farley Ho modificato la domanda per essere più chiari
Steffen Mangold,

Con il partizionamento, troverai che fa tutto ciò che cerca in ogni partizione. Con il predicato aggiuntivo, puoi farlo in modo che non si preoccupi di tutti loro e ne faccia solo alcuni. Fallo un mese, se necessario.
Rob Farley,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.