web-dev-qa-db-fra.com

Accélérer le nombre (*) sur les grandes tables

Nous utilisons une application fournisseur s'exécutant sur SQL Server Enterprise, et elle a une bizarrerie plutôt ennuyeuse d'exécuter des instructions COUNT sur la table Items lors du traitement de la plupart des documents financiers (commandes, factures, etc.).

Par exemple. SELECT COUNT('A') FROM [dbo].[Items] T0

Je suis sûr que ce serait normalement bien, mais il y a plus de 6 millions d'enregistrements, et il faut environ 400 ms pour les compter tous. Cela peut constituer une partie substantielle du temps de traitement global.

La table a déjà un index NonClustered extrêmement étroit (tinyint, plus Clustered Key) qui est ce que SQL utilise quand il fait le scan de table, donc je ne pense pas que nous puissions faire mieux à cet égard.

Il y a quelques solutions que je connais, que nous aimerions éviter si possible:

Avons-nous d'autres options pour accélérer cela?

Voici un Gist montrant la configuration: https://Gist.github.com/elvishfiend/5094f120b14f8ecfb325623edcb5f3eb

8
Zac Faragher

La vue indexée devrait être parmi les options les plus rapides, avec la surcharge de maintenance la plus faible, lorsqu'elle est mise en œuvre de manière optimale .

Les modifications sont incrémentielles (deltas) comme je l'explique en détail dans Maintenance de la vue indexée dans les plans d'exécution (un recomptage complet n'est pas effectué à chaque mise à jour de la table de base); cependant, vous devez vous assurer que les parties de mise à jour delta du plan d'exécution disposent de méthodes d'accès efficaces (comme toute requête).

Il est généralement assez simple d'identifier un index manquant dans le plan d'exécution INSERT/UPDATE/DELETE. Vous pourriez peut-être ajouter un plan d'exécution illustratif (réel) à votre question.

La correspondance automatique du texte de la requête à une vue indexée n'est disponible que dans Enterprise Edition (et équivalents). Dans les autres éditions, vous devez utiliser l'indicateur de table WITH (NOEXPAND). Il existe également bonnes raisons pour utiliser NOEXPAND même sur Enterprise Edition.

Concernant le code de démonstration: assurez-vous de spécifier l'indice à l'aide de WITH (NOEXPAND). De la façon dont vous l'avez écrit, NOEXPAND est analysé comme un alias. Notez également que seules les vues matérialisées (indexées) peuvent avoir un indice NOEXPAND.

Si vous ne parvenez pas à ajouter un indice directement, ce serait une excellente utilisation d'un guide de plan. Un guide de plan peut également être utilisé pour garantir qu'une requête qui correspond à une vue indexée (sans la nommer explicitement) utilise réellement la vue indexée.

N'oubliez pas que sans NOEXPAND sur une vue matérialisée (indexée), SQL Server étend toujours la définition de la vue au début de la compilation du plan. Enterprise Edition peut (ou peut ne pas) faire correspondre (parties de) une requête à une vue indexée en fonction de son évaluation des coûts de chaque option.

Questions et réponses connexes:

14
Paul White 9

Si vous êtes bloqué sur SQL Server 2012, vous pouvez essayer de créer un index uniquement sur la clé d'index cluster. Il peut être un peu plus petit qu'un index sur une colonne TINYINT. Vous pouvez également essayer d'ajouter la compression de page à votre index. Cela pourrait rendre votre requête plus rapide, mais cela dépend des données du tableau.

Si vous pouvez mettre à niveau vers SQL Server 2016, vous pouvez créer un index columnstore non cluster sur la table. Cela rendra les requêtes COUNT(*) extrêmement rapides avec un surcoût inférieur sur les opérations DML. Voici une démo rapide:

DROP TABLE IF EXISTS #Items;

CREATE TABLE #Items (
    CLUST_KEY BIGINT NOT NULL,
    SMALL_COLUMN TINYINT NOT NULL,
    FILLER VARCHAR(50) NOT NULL,
    PRIMARY KEY (CLUST_KEY)
);

INSERT INTO #Items WITH (TABLOCK)
SELECT
    ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
    , 1
    , REPLICATE('Z', 50)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;

CREATE INDEX NCI ON #Items (SMALL_COLUMN);

SET STATISTICS TIME ON;

-- CPU time = 312 ms,  elapsed time = 320 ms.
SELECT COUNT(*)
FROM #Items
OPTION (MAXDOP 1);


CREATE NONCLUSTERED COLUMNSTORE INDEX NCCI ON #Items (SMALL_COLUMN);

-- CPU time = 0 ms,  elapsed time = 1 ms.
SELECT COUNT(*)
FROM #Items
OPTION (MAXDOP 1);

Avec le NCCI, je peux compter six millions de lignes en moins de 20 ms.

6
Joe Obbish
  1. vous pouvez ajouter une colonne id que vous mettez à jour manuellement de telle sorte que soit toujours en séquence la pluie, l'été ou l'hiver.

  2. Si vous avez une table unique et qu'il n'y a pas de condition ou de jointure, alors

     SELECT o.NAME
    ,o.schema_id
    ,ddps.row_count
    FROM sys.indexes AS i
    INNER JOIN sys.objects AS o ON i.OBJECT_ID = o.OBJECT_ID
    INNER JOIN sys.dm_db_partition_stats AS ddps ON i.OBJECT_ID = ddps.OBJECT_ID
      AND i.index_id = ddps.index_id
    WHERE i.index_id < 2
      AND o.is_ms_shipped = 0
      AND o.NAME = 'even'
      AND o.schema_id = 1
    

J'entends que cela ne dépend pas de statistiques mises à jour. Je ne suis pas sûr.

  1. sp_spaceused employee Cela dépend des statistiques mises à jour.

  2. vous pouvez créer quelque chose comme un travail qui s'exécute une fois et stocker le dernier identifiant et compter

    créer la table ItemCount comme Latestid int non null, LatestCount int non null

La table Itemcount contient toujours seulement 1 lignes et aucun index n'est nécessaire

insert into ItemCount (Latestid,LatestCount)
select top 1  itemid
,(select count(*) from [dbo].[Items])LatestCount
from [dbo].[Items]
order by itemid DESC

- Ici, la logique de comptage est la vôtre

donc chaque fois que votre requête nécessite un décompte, vous pouvez le faire,

declare @LatestID INT
declare @LatestCount int

select @LatestID=LatestID,@LatestCount=LatestCount 
from ItemCount ic 

declare @FreshCount int
declare @NewCount int
SELECT @FreshCount=COUNT(1)  FROM [dbo].[Items] it
where it.itemid>=@LatestID 

set @NewCount=@FreshCount+@LatestCount

ici la colonne itemid de [dbo].[Items] doit être indexée

Cela conviendra également si vous avez la condition join and filter Dans votre requête count (*)

1
KumarHarsh