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

8

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

entrez la description de l'image ici

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ée 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 BYou une SELECT OVER PARTITION BYrequête. 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 d'E / S 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 une [End]colonne supérieure 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 que cela a également un impact très important sur les E / S 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 toute la [OperationData]table.

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

ÉCHOUÉ!

Steffen Mangold
la source
2
Dans votre premier bloc de code, je ne vois pas d'où vient la première colonne de l'index clusterisé - est-ce vrai?
George.Palacios
Oui désolé SSMS ne l'inclut pas dans le CREATE TABLEscript mais à l'intérieur du plan de requête, vous verrez les partitions. Je vais modifier la question.
Steffen Mangold
Pas un index supplémentaire, car inclus à l'intérieur, PRIMARY KEY CLUSTEREDvous pensez que cela peut aider?
Steffen Mangold
Soryy c'était une erreur, j'ai modifié les noms de la question par plus clair, je l'ai corrigée.
Steffen Mangold
@ ypercubeᵀᴹ oui parce que SELECT [SourceID], [Source], [End] FROM insertedcertains comment faire un scan de table sur le [OperationData].
Steffen Mangold

Réponses:

9

Si vous avez une table de SourceIDvaleurs et un index sur votre table principale (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 ne recherchez que votre dernière partition, vous pouvez inclure un filtre à la fin, comme AND d.[End] > DATEADD(day, -1, GETDATE())

Modifier: Parce que votre index cluster est activé 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;
Rob Farley
la source
L'index a vraiment accéléré la requête. Un deuxième problème qui se pose avec lui est qu'un index non partitionné sur une telle table énorme est presque impossible à maintenir. Sur l'ensemble de notre table "big-data" nous travaillons avec un indexeur partitionné. Ils peuvent être maintenus en ligne partition par partition. Dès que l'indexeur est partitionné, le problème est le plus ancien car il doit parcourir toutes les partitions.
Steffen Mangold
1
@SteffenMangold: moins il y a de données dans un index, mieux c'est (tant qu'il contient tout ce dont vous avez besoin) et en excluant les vues matérialisées, l'index cluster a le maximum de données possible. Des index clusterisés sont présents car l'obtention de toutes les données par la clé est la norme. Dans ce cas, vous obtenez toutes les données, mais vous ne les obtenez pas vraiment par la clé, vous les obtenez par une partie de la clé. Vous avez besoin d'un index qui peut être interrogé avec une partie de la clé.
jmoreno
Je suis vraiment désolé mais il y a un Sourcetableau référençant la sourceIDcolonne. La source de la colonne n'est qu'un nom de fichier. C'est un nom un peu déroutant. Pour chaque Sourcepériphérique (sourceID), il ne peut y avoir qu'une seule entrée pour un fichier source(colonne) à un horodatage. De plus, je ne peux pas éliminer les partitions car la plus récente Endest largement fragmentée. C'est pourquoi j'ai trouvé la solution de déclenchement. Je pense qu'une requête en direct ne fonctionnera pas ici.
Steffen Mangold
@Rob Farley J'ai modifié la question pour être plus claire
Steffen Mangold
Avec le partitionnement, vous constaterez qu'il fait toutes ces recherches dans chaque partition. Avec le prédicat supplémentaire, vous pouvez faire en sorte qu'il ne dérange pas tous, et n'en fasse que quelques-uns. Faites-en un mois si vous en avez besoin.
Rob Farley