web-dev-qa-db-fra.com

HAUT (1) PAR GROUPE d'une très grande table (100 000 000+)

Installer

J'ai une énorme table d'environ 115 382 254 lignes. Le tableau est relativement simple et enregistre les opérations du processus d'application.

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

Le tableau est regroupé en environ 500 grappes et sur une base quotidienne.

partitions

enter image description here

De plus, le tableau est bien indexé par PK, les statistiques sont à jour et l'INDEXer est défragmenté chaque nuit.

Les SELECT basés sur un index sont rapides comme l'éclair et nous n'avons eu aucun problème avec cela.

Problème

J'ai besoin de connaître la dernière ligne (TOP) par [End] et partitionné par [SourceDeciveID]. Pour obtenir le dernier [OperationData] de chaque périphérique source.

Question

Je dois trouver un moyen de résoudre ce problème dans le bon sens et sans amener la DB aux limites.


Effort 1

Le premier essai était évident GROUP BY ou SELECT OVER PARTITION BY requete. Le problème ici est également évident, chaque requête doit parcourir l'ordre des partitions/trouver la ligne du haut. La requête est donc très lente et a un impact IO très élevé.

Exemple de requête 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

Exemple de requête 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

ÉCHOUÉ!

Effort 2

J'ai créé une table d'aide pour toujours contenir une référence à la ligne 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])

Pour remplir le tableau, vous avez créé un déclencheur pour toujours ajouter/mettre à jour la ligne source si elle est supérieure [End] la colonne est insérée.

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

Le problème ici est qu'il a également un très grand impact IO et je ne sais pas pourquoi.

Comme vous pouvez le voir ici dans le plan de requête il exécute également une analyse sur l'ensemble [OperationData] table.

Cela a un impact global énorme sur ma base de données. stats

ÉCHOUÉ!

8
Steffen Mangold

Si vous avez une table de valeurs SourceID et un index sur votre table principale sur (SourceID, End) include (othercolumns), Utilisez simplement 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;

Si vous savez que vous n’êtes qu’après votre dernière partition, vous pouvez inclure un filtre à la fin, comme AND d.[End] > DATEADD(day, -1, GETDATE())

Edit: Parce que votre index cluster est sur SourceID, Source, End), Placez également Source dans votre table Sources et joignez-vous à cela également. Ensuite, vous n'avez pas besoin du nouvel index.

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;
9
Rob Farley