Est-il possible d'augmenter les performances des requêtes sur une table étroite avec des millions de lignes?

14

J'ai une requête qui prend actuellement en moyenne 2500 ms pour être terminée. Ma table est très étroite, mais il y a 44 millions de lignes. Quelles options dois-je pour améliorer les performances, ou est-ce aussi bon que possible?

La requête

SELECT TOP 1000 * FROM [CIA_WIZ].[dbo].[Heartbeats]
WHERE [DateEntered] BETWEEN '2011-08-30' and '2011-08-31'; 

La table

CREATE TABLE [dbo].[Heartbeats](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [DeviceID] [int] NOT NULL,
    [IsPUp] [bit] NOT NULL,
    [IsWebUp] [bit] NOT NULL,
    [IsPingUp] [bit] NOT NULL,
    [DateEntered] [datetime] NOT NULL,
 CONSTRAINT [PK_Heartbeats] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

L'index

CREATE NONCLUSTERED INDEX [CommonQueryIndex] ON [dbo].[Heartbeats] 
(
    [DateEntered] ASC,
    [DeviceID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]

L'ajout d'index supplémentaires aiderait-il? Si oui, à quoi ressembleraient-ils? Les performances actuelles sont acceptables, car la requête n'est exécutée qu'occasionnellement, mais je me demande, en tant qu'exercice d'apprentissage, que puis-je faire pour accélérer le processus?

MISE À JOUR

Lorsque je modifie la requête pour utiliser un indice d'index de force, la requête s'exécute en 50 ms:

SELECT TOP 1000 * FROM [CIA_WIZ].[dbo].[Heartbeats] WITH(INDEX(CommonQueryIndex))
WHERE [DateEntered] BETWEEN '2011-08-30' and '2011-08-31' 

L'ajout d'une clause DeviceID correctement sélective atteint également la plage de 50 ms:

SELECT TOP 1000 * FROM [CIA_WIZ].[dbo].[Heartbeats]
WHERE [DateEntered] BETWEEN '2011-08-30' and '2011-08-31' AND DeviceID = 4;

Si j'ajoute ORDER BY [DateEntered], [DeviceID]à la requête d'origine, je suis dans la plage des 50 ms:

SELECT TOP 1000 * FROM [CIA_WIZ].[dbo].[Heartbeats]
WHERE [DateEntered] BETWEEN '2011-08-30' and '2011-08-31' 
ORDER BY [DateEntered], [DeviceID];

Tout cela utilise l'index que j'attendais (CommonQueryIndex) donc, je suppose que ma question est maintenant, existe-t-il un moyen de forcer cet index à être utilisé sur des requêtes comme celle-ci? Ou la taille de ma table rejette-t-elle trop l'optimiseur et je dois simplement utiliser un ORDER BYou un indice?

Nate
la source
Je suppose que vous pourriez ajouter un index non clusterisé supplémentaire sur "DateEntered", ce qui augmenterait les performances dans une plus grande mesure
Praveen
@Praveen Serait-il fondamentalement le même que mon index existant? Dois-je faire quelque chose de spécial car il y aura deux index sur le même champ?
Nate
@Nate, puisque la table s'appelle battement de cœur et qu'il y a 44 millions d'enregistrements impliqués, je suppose que vous avez des encarts lourds sur cette table? Avec l'indexation, vous ne pouvez ajouter qu'un index de couverture pour accélérer. Mais comme vous l'avez mentionné, vous n'utilisez cette requête que de temps en temps, je vous le déconseille fortement si vous effectuez des insertions lourdes. Il double essentiellement votre charge d'insertion. Utilisez-vous l'édition Enterprise?
Edward Dortland
J'ai remarqué que vous avez deviceID dans votre index NC. Est-il possible d'inclure cela dans votre clause where? Et cela ferait-il baisser le résultat fixé en dessous du seuil? <35k enregistrements (sans la clause 1000 supérieure).
Edward Dortland
1
dernière question, insérez-vous toujours par ordre de date Ou peuvent-ils être hors service, car les appareils peuvent s'insérer les uns les autres. Vous pouvez essayer de remplacer l'index clusterisé par la colonne DateEntered. Vos pages de congé de votre index cluster sont maintenant de 445 pages. Cela doublerait, si vous passiez d'un int à un datetime. Mais dans ce cas, ce n'est peut-être pas si mal.
Edward Dortland

Réponses:

13

Pourquoi l'optimiseur ne choisit pas votre premier index:

CREATE NONCLUSTERED INDEX [CommonQueryIndex] ON [dbo].[Heartbeats] 
(
    [DateEntered] ASC,
    [DeviceID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]

Est une question de sélectivité de la colonne [DateEntered].

Vous nous avez dit que votre table compte 44 millions de lignes. la taille de la ligne est:

4 octets, pour l'ID, 4 octets pour l'ID de périphérique, 8 octets pour la date et 1 octet pour les colonnes de 4 bits. cela représente 17 octets + 7 octets de surcharge pour (balises, bitmap nul, décalage de col var, nombre de col) totalisant 24 octets par ligne.

Cela se traduirait grossièrement par 140 000 pages. Pour stocker ces 44 millions de lignes.

L'optimiseur peut maintenant faire deux choses:

  1. Il pourrait analyser la table (analyse d'index en cluster)
  2. Ou il pourrait utiliser votre index. Pour chaque ligne de votre index, il devrait alors effectuer une recherche de signet dans l'index clusterisé.

À un certain moment, il devient plus coûteux d'effectuer toutes ces recherches uniques dans l'index cluster pour chaque entrée d'index trouvée dans votre index non cluster. Le seuil pour cela est généralement le nombre total de recherches doit dépasser 25% à 33% du nombre total de pages de table.

Donc dans ce cas: 140k / 25% = 35000 lignes 140k / 33% = 46666 lignes.

(@RBarryYoung, 35k représente 0,08% du total des lignes et 46666 est 0,10%, donc je pense que c'est là que la confusion était)

Donc, si votre clause where se traduira par quelque part entre 35000 et 46666 lignes (c'est sous la clause supérieure!), Il est très probable que votre non cluster ne sera pas utilisé et que l'analyse d'index cluster sera utilisée.

Les deux seules façons de changer cela sont:

  1. Rendez votre clause where plus sélective. (si possible)
  2. Déposez le * et sélectionnez seulement quelques colonnes pour pouvoir utiliser un index de couverture.

maintenant sûr que vous pouvez créer un index de couverture même lorsque vous utilisez un select *. Cependant, cela ne fait que créer une surcharge énorme pour vos insertions / mises à jour / suppressions. Il nous faudrait en savoir plus sur votre charge de travail (lecture vs écriture) pour vous assurer que c'est la meilleure solution.

Le passage de datetime à smalldatetime représente une réduction de 16% de la taille de l'index cluster et une réduction de 24% de la taille de votre index non cluster.

Edward Dortland
la source
le seuil de balayage est normalement beaucoup plus bas que cela (10% ou même plus bas), mais comme la plage est un seul jour d'il y a plus d'un an, il ne devrait même pas atteindre ce seuil. Et une analyse d'index en cluster n'est pas une donnée, car un index de couverture a été ajouté. Étant donné que cet index rend la clause WHERE SARG-capable, il devrait être préféré.
RBarryYoung
@RBarryYoung J'essayais d'expliquer pourquoi l'index non clusterisé sur [EnteredDate], [DeviceID] n'était pas utilisé en premier lieu. En ce qui concerne le seuil, je pense que nous sommes tous les deux d'accord, je ne parle que du point de vue de la page. Je vais modifier ma réponse pour que ce soit plus clair.
Edward Dortland
Modification de la réponse pour clarifier ce à quoi je répondais. Je ne peux pas expliquer pourquoi l'index de couverture suggéré par @RBarryYoung n'est pas utilisé. Je l'ai testé sur un million de lignes juste ici, et je l'ai optimisé en utilisant l'indice de couverture.
Edward Dortland
Merci pour une réponse très complète, cela a beaucoup de sens. En ce qui concerne la charge de travail, le tableau comporte 150 à 300 insertions par période de 5 minutes et quelques lectures par jour à des fins de rapport.
Nate
Les frais généraux pour l'indice de couverture ne sont pas vraiment significatifs étant donné qu'il s'agit d'un tableau étroit et que la "couverture" n'est qu'un ajout à l'indice préexistant qui comprenait déjà la majeure partie de la ligne.
RBarryYoung
8

Y a-t-il une raison particulière pour laquelle votre PK est en cluster? Beaucoup de gens le font parce que c'est par défaut de cette façon, ou ils pensent que les PK doivent être groupés. Non. Les index clusterisés sont généralement les meilleurs pour les requêtes de plage (comme celle-ci) ou sur la clé étrangère d'une table enfant.

Un indice de clustering a pour effet de regrouper toutes les données car les données sont stockées sur les nœuds terminaux de l'arborescence du cluster b. Donc, en supposant que vous ne demandez pas une plage «trop large», l'optimiseur saura exactement quelle partie de l'arborescence b contient les données et il n'aura pas à trouver un identificateur de ligne, puis sautera jusqu'à l'endroit où les données est (comme il le fait lorsqu'il s'agit d'un index NC). Qu'est-ce qui est «trop large» d'une gamme? Un exemple ridicule serait de demander 11 mois de données à partir d'un tableau qui n'a que la valeur d'un an de dossiers. Tirer un jour de données ne devrait pas être un problème, en supposant que vos statistiques soient à jour. (Cependant, l'optimiseur peut avoir des problèmes si vous recherchez les données d'hier et que vous n'avez pas mis à jour les statistiques depuis trois jours.)

Étant donné que vous exécutez une requête "SELECT *", le moteur devra renvoyer toutes les colonnes de la table (même si quelqu'un en ajoute une nouvelle dont votre application n'a pas besoin à ce moment), donc un index de couverture ou un index avec des colonnes incluses n'aidera pas beaucoup, voire pas du tout. (Si vous incluez chaque colonne de la table dans un index, vous faites quelque chose de mal.) L'optimiseur ignorera probablement ces index NC.

Alors que faire?

Ma suggestion serait de supprimer l'index NC, de changer le cluster PK en non cluster et de créer un index cluster sur [DateEntered]. Plus c'est simple, mieux c'est, jusqu'à preuve du contraire.

détroit de darin
la source
En supposant que les lignes sont insérées dans l'ordre croissant, c'est la réponse la plus simple - mais l'insertion dans un ordre non linéaire entraînera une fragmentation.
Kirk Broadhurst
L'ajout de données à toute structure b-tree entraînera une perte d'équilibre. Même si vous ajoutez des lignes dans l'ordre des clusters, les index perdront leur équilibre. La réindexation des tables supprime la fragmentation et tout administrateur de base de données vous dira que les tables doivent être réindexées après que «suffisamment» de données ont été ajoutées à une table. (La définition de "assez" pourrait être débattue, ou "quand" pourrait être une discussion.) Je ne vois rien dans la question qui dit que la réindexation ne peut pas être faite pour une raison quelconque.
Détroit de Darin
4

Tant que vous avez ce "*" là-dedans, alors la seule chose que je pourrais imaginer qui ferait une grande différence serait de changer votre définition d'index en ceci:

CREATE NONCLUSTERED INDEX [CommonQueryIndex] ON [dbo].[Heartbeats] 
(
    [DateEntered] ASC,
    [DeviceID] ASC
)INCLUDE (ID, IsWebUp, IsPingUp, IsPUp)
 WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]

Comme je l'ai noté dans les commentaires, il devrait utiliser cet index, mais si ce n'est pas le cas, vous pouvez le persuader avec un ORDER BY ou un indice.

RBarryYoung
la source
Je viens de l'essayer et je suis toujours à peu près au même endroit, 2500 ms d'attente pour la réponse du serveur et 10 ms de temps de processus client.
Nate
Publiez le plan de requête.
RBarryYoung
On dirait qu'il utilise l'index clusterisé. (Coût SELECT: 0% <- Coût supérieur: 20% <- Scan d'index clusterisé Coût PK_Heartbeats: 80%)
Nate
Ouais, ce n'est pas vrai, quelque chose de désactiver les statistiques / optimiseur. Ajoutez un indice pour le forcer à utiliser le nouvel index.
RBarryYoung
@Max Vernon: Peut-être, mais cela aurait dû être signalé sur le plan de requête.
RBarryYoung
3

Je regarderais cela un peu différemment.

  • Oui, je sais que c'est un vieux fil mais je suis intrigué.

Je viderais la colonne datetime - la changer en un int. Ayez une table de recherche ou faites une conversion pour votre date.

Vider l'index clusterisé - le laisser comme un tas et créer un index non clusterisé sur la nouvelle colonne INT qui représente la date. c'est-à-dire qu'aujourd'hui serait 20121015. Cet ordre est important. Selon la fréquence à laquelle vous chargez la table, essayez de créer cet index dans l'ordre DESC. Le coût de maintenance sera plus élevé et vous voudrez introduire un facteur de remplissage ou de partitionnement. Le partitionnement aiderait également à réduire votre temps d'exécution.

Enfin, si vous pouvez utiliser SQL 2012, essayez d'utiliser SEQUENCE - il surpassera l'identité () pour les insertions.

Jeremy Lowell
la source
Solution intéressante. Bien que cela ne ressorte pas de ma question, la partie heure de la date et de l'heure est très importante. Généralement, je demande en fonction de la date, pour passer en revue des moments spécifiques au cours de cette période. Comment ajusteriez-vous cette solution pour en tenir compte?
Nate
Dans ce cas, conservez la colonne datetime, ajoutez la colonne int pour date (car votre plage est basée sur l'élément date et non sur l'élément time). Vous pouvez également envisager d'utiliser le type de données TIME, puis de diviser efficacement l'heure en dehors de la date. De cette manière, votre empreinte de données est plus petite et vous avez toujours l'élément Time de la colonne.
Jeremy Lowell
1
Je ne sais pas pourquoi j'ai manqué cela plus tôt, mais utilisez également la compression des lignes sur l'index cluster et l'index non cluster. Je viens de faire un test rapide avec votre table et voici ce que j'ai trouvé: j'ai créé un ensemble de données (5,8 millions de lignes) dans le tableau défini ci-dessus. J'ai compressé (ligne) l'index cluster et non cluster. les lectures logiques, basées sur votre requête exacte, sont passées de 2 074 à 1 433. C'est une diminution importante et je suis convaincu que seul pourrait vous aider - et c'est un risque très faible.
Jeremy Lowell