Bogue de performances d'index datetime SQL Server 2008

11

Nous utilisons SQL Server 2008 R2 et avons une très grande table (100M + lignes) avec un index d'ID principal et une datetimecolonne avec un index non cluster. Nous constatons un comportement client / serveur très inhabituel basé sur l'utilisation d'une order byclause spécifiquement sur une colonne datetime indexée .

J'ai lu l'article suivant: /programming/1716798/sql-server-2008-ordering-by-datetime-is-too-slow mais il se passe plus de choses avec le client / serveur que ce qui est commencer décrit ici.

Si nous exécutons la requête suivante (modifiée pour protéger certains contenus):

select * 
from [big table] 
where serial_number = [some number] 
order by test_date desc

La requête expire à chaque fois. Dans le SQL Server Profiler, la requête exécutée ressemble à ceci au serveur:

exec sp_cursorprepexec @p1 output,@p2 output,NULL,N'select * .....

Maintenant, si vous modifiez la requête en, dites ceci:

declare @temp int;
select * from [big table] 
where serial_number = [some number] 
order by test_date desc

Le SQL Server Profiler montre la requête exécutée comme ceci au serveur, et cela FONCTIONNE instantanément:

exec sp_prepexec @p1 output, NULL, N'declare @temp int;select * from .....

En fait, vous pouvez même mettre un commentaire vide ('-;') au lieu d'une instruction declare inutilisée et obtenir le même résultat. Donc, au départ, nous pointions le préprocesseur sp comme cause première de ce problème, mais si vous faites cela:

select * 
from [big table] 
where serial_number = [some number] 
order by Cast(test_date as smalldatetime) desc

Il fonctionne également instantanément (vous pouvez le lancer comme n'importe quel autre datetimetype), retournant le résultat en millisecondes. Et le profileur affiche la demande au serveur sous la forme:

exec sp_cursorprepexec @p1 output, @p2 output, NULL, N'select * from .....

Cela exclut donc quelque peu la sp_cursorprepexecprocédure de la cause complète du problème. Ajoutez à cela le fait que le sp_cursorprepexecest également appelé lorsqu'aucune commande par n'est utilisée et le résultat est également retourné instantanément.

Nous avons beaucoup cherché sur ce problème, et je vois des problèmes similaires publiés par d'autres, mais aucun qui le décompose à ce niveau.

Les autres ont-ils donc été témoins de ce comportement? Quelqu'un a-t-il une meilleure solution que de mettre du SQL vide de sens devant l'instruction select pour changer le comportement? Étant donné que SQL Server doit invoquer la commande après la collecte des données, il semble bien que ce soit un bogue sur le serveur qui persiste depuis longtemps. Nous avons constaté que ce comportement est cohérent dans plusieurs de nos grandes tables et qu'il est reproductible.

Modifications:

Je dois également ajouter que la mise forceseeken place fait également disparaître le problème.

Je devrais ajouter pour aider les chercheurs, l'erreur de délai d'expiration ODBC levée est: [Microsoft] [Pilote ODBC SQL Server] Opération annulée

Ajouté le 12/12/2012: Toujours à la recherche de la cause première (en plus d'avoir construit un échantillon à donner à Microsoft, je posterai les résultats ici après avoir soumis) J'ai creusé dans le fichier de trace ODBC entre une requête de travail (avec une déclaration de commentaire / déclaration ajoutée) et une requête qui ne fonctionne pas. La différence de trace fondamentale est affichée ci-dessous. Il se produit lors de l'appel à l'appel SQLExtendedFetch une fois toutes les discussions SQLBindCol terminées. L'appel échoue avec le code retour -1, et le thread parent entre ensuite dans SQLCancel. Étant donné que nous pouvons produire cela avec les pilotes ODBC client natif et hérité, je signale toujours un problème de compatibilité côté serveur.

(clip)
MSSQLODBCTester 1664-1718   EXIT  SQLBindCol  with return code 0 (SQL_SUCCESS)
        HSTMT               0x001EEA10
        UWORD                       16 
        SWORD                        1 <SQL_C_CHAR>
        PTR                0x03259030
        SQLLEN                    51
        SQLLEN *            0x0326B820 (0)

MSSQLODBCTester 1664-1718   ENTER SQLExtendedFetch 
        HSTMT               0x001EEA10
        UWORD                        1 <SQL_FETCH_NEXT>
        SQLLEN                     1
        SQLULEN *           0x032677C4
        UWORD *             0x032679B0

MSSQLODBCTester 1664-1fd0   ENTER SQLCancel 
        HSTMT               0x001EEA10

MSSQLODBCTester 1664-1718   EXIT  SQLExtendedFetch  with return code -1 (SQL_ERROR)
        HSTMT               0x001EEA10
        UWORD                        1 <SQL_FETCH_NEXT>
        SQLLEN                     1
        SQLULEN *           0x032677C4
        UWORD *             0x032679B0

        DIAG [S1008] [Microsoft][ODBC SQL Server Driver]Operation canceled (0) 

MSSQLODBCTester 1664-1fd0   EXIT  SQLCancel  with return code 0 (SQL_SUCCESS)
        HSTMT               0x001EEA10

MSSQLODBCTester 1664-1718   ENTER SQLErrorW 
        HENV                0x001E7238
        HDBC                0x001E7B30
        HSTMT               0x001EEA10
        WCHAR *             0x08BFFC5C
        SDWORD *            0x08BFFF08
        WCHAR *             0x08BFF85C 
        SWORD                      511 
        SWORD *             0x08BFFEE6

MSSQLODBCTester 1664-1718   EXIT  SQLErrorW  with return code 0 (SQL_SUCCESS)
        HENV                0x001E7238
        HDBC                0x001E7B30
        HSTMT               0x001EEA10
        WCHAR *             0x08BFFC5C [       5] "S1008"
        SDWORD *            0x08BFFF08 (0)
        WCHAR *             0x08BFF85C [      53] "[Microsoft][ODBC SQL Server Driver]Operation canceled"
        SWORD                      511 
        SWORD *             0x08BFFEE6 (53)

MSSQLODBCTester 1664-1718   ENTER SQLErrorW 
        HENV                0x001E7238
        HDBC                0x001E7B30
        HSTMT               0x001EEA10
        WCHAR *             0x08BFFC5C
        SDWORD *            0x08BFFF08
        WCHAR *             0x08BFF85C 
        SWORD                      511 
        SWORD *             0x08BFFEE6

MSSQLODBCTester 1664-1718   EXIT  SQLErrorW  with return code 100 (SQL_NO_DATA_FOUND)
        HENV                0x001E7238
        HDBC                0x001E7B30
        HSTMT               0x001EEA10
        WCHAR *             0x08BFFC5C
        SDWORD *            0x08BFFF08
        WCHAR *             0x08BFF85C 
        SWORD                      511 
        SWORD *             0x08BFFEE6
(clip)

Ajout d'un cas Microsoft Connect 10/12/2012:

https://connect.microsoft.com/SQLServer/feedback/details/767196/order-by-datetime-in-odbc-fails-for-clean-sql-statements#details

Je dois également noter que nous avons recherché les plans de requête pour les requêtes fonctionnelles et non fonctionnelles. Ils sont tous deux réutilisés de manière appropriée en fonction du nombre d'exécutions. Le vidage des plans mis en cache et la réexécution ne modifient pas le succès de la requête.

DBtheDBA
la source
Que se passe-t-il si vous essayez select id, test_date from [big table] where serial_number = ..... order by test_date- je me demande simplement si cela SELECT *a un impact négatif sur vos performances. Si vous avez un index non clusterisé test_dateet un index clusterisé id(en supposant que c'est ainsi qu'on l'appelle), cette requête devrait être couverte par cet index non clusterisé et devrait donc revenir assez rapidement
marc_s
Désolé, bon point. J'aurais dû inclure que nous avons essayé de modifier l'espace de la colonne sélectionnée (en supprimant le '*', etc.) fortement avec diverses combinaisons. Le comportement décrit ci-dessus a persisté à travers ces changements.
DBtheDBA
J'ai maintenant lié mes comptes à ce site. Si un modérateur veut déplacer le message vers ce site, je vais bien de toute façon. Un de mes développeurs m'a signalé ce site après avoir posté ici.
DBtheDBA
Quelle pile de clients est utilisée ici? Sans l'intégralité du texte de trace, cela semble être le problème. Essayez d'envelopper l'appel d'origine à l'intérieur sp_executesqlet voyez ce qui se passe.
Jon Seigel
1
À quoi ressemble le plan d'exécution lente? Reniflage de paramètres?
Martin Smith

Réponses:

6

Il n'y a pas de mystère, vous obtenez un bon (euh) ou (vraiment) mauvais plan au hasard car il n'y a pas de choix clair pour l'index à utiliser. Bien que contraignant pour la clause ORDER BY et évitant ainsi le tri, votre index non cluster sur la colonne datetime est un très mauvais choix pour cette requête. Ce qui ferait un bien meilleur index pour cette requête en serait un (serial_number, test_date). Encore mieux, cela ferait un très bon candidat pour une clé d'index cluster .

En règle générale, les séries temporelles doivent être regroupées par colonne de temps, car l'écrasante majorité des demandes s'intéresse à des plages de temps spécifiques. Si les données sont également intrinsèquement partitionnées sur une colonne avec une faible sélectivité, comme cela semble être le cas avec votre numéro de série, cette colonne doit être ajoutée comme la plus à gauche dans la définition de clé en cluster.

Remus Rusanu
la source
Je suis un peu confus ici. Pourquoi le plan serait-il fondé sur une the orderclause? Le plan ne doit-il pas se limiter aux whereconditions, car la commande ne doit avoir lieu qu'après la récupération des lignes? Pourquoi le serveur essaierait-il de trier les enregistrements avant d'avoir l'ensemble des résultats?
DBtheDBA
5
Cela n'explique pas non plus pourquoi l'ajout d'un commentaire au début de la requête affecte la durée d'exécution.
cfradenburg
De plus, nos tables sont presque toujours interrogées par numéro de série et non par date_test. Nous avons des index non clusterisés sur les deux et un cluster uniquement sur la colonne id de la table. Il s'agit d'un magasin de données opérationnel, et l'ajout d'index clusterisés sur d'autres colonnes ne ferait que générer des fractionnements de page et de moins bonnes performances.
DBtheDBA
1
@DBtheDBA: si vous voulez faire une réclamation pour un «bug», vous devez faire une enquête et une divulgation appropriées. Le schéma exact de votre table et les statistiques exportées, suivez Comment générer un script des métadonnées de base de données nécessaires pour créer une base de données statistiques uniquement dans SQL Server 2005 et SQL Server 2008 , en particulier les statistiques de script toutes importantes : statistiques de script et histogrammes . Ajoutez-les aux informations de publication ainsi que les étapes qui reproduisent le problème.
Remus Rusanu
1
Nous l'avons lu auparavant lors de nos recherches, et je comprends ce que vous dites, mais il y a une faille fondamentale dans quelque chose que le serveur fait ici. Nous avons reconstruit la table et les index et l'avons reproduit sur une nouvelle table. L'option de recompilation ne résout pas le problème, ce qui est un gros indice que quelque chose ne va pas. Je ne doute pas que la mise en place d'index clusterisés sur tout pourrait potentiellement résoudre ce problème, mais ce n'est pas une solution à la cause première, c'est une solution de contournement et une solution coûteuse sur une grande table.
DBtheDBA
0

Documentez les détails sur la façon de reproduire le bogue et soumettez-le sur connect.microsoft.com. J'ai vérifié et je ne voyais déjà rien qui pourrait être lié à cela.

cfradenburg
la source
Je demanderai à mon DBA de taper un script demain pour créer un environnement à reproduire. Je ne pense pas que ce soit si difficile. Je le publierai ici également si quelqu'un est intéressé à l'essayer lui-même.
DBtheDBA
Publiez également l'élément de connexion lorsqu'il est ouvert. De cette façon, si quelqu'un d'autre a ce problème, il est directement signalé. Et tous ceux qui regardent cette question voudront peut-être voter pour l'élément, donc Microsoft est plus susceptible d'y prêter attention.
cfradenburg
0

Mon hypothèse est que vous utilisez le cache du plan de requête. (Remus pourrait dire la même chose que moi, mais d'une manière différente.)

Voici une tonne de détails sur la façon dont SQL planifie la mise en cache .

Glisser sur les détails: quelqu'un a exécuté cette requête plus tôt, pour un [certain nombre] particulier. SQL a examiné la valeur fournie, les index et les statistiques pour la table / les colonnes pertinentes, etc. et a élaboré un plan qui fonctionnait bien pour ce particulier [un certain nombre]. Il a ensuite mis le plan en cache, l'a exécuté et a rendu les résultats à l'appelant.

Plus tard, quelqu'un d'autre exécute la même requête, pour une valeur différente de [un certain nombre]. Cette valeur particulière entraîne un nombre très différent de lignes de résultats et le moteur doit créer un plan différent pour cette instance de la requête. Mais cela ne fonctionne pas de cette façon. Au lieu de cela, SQL prend la requête et (plus ou moins) effectue une recherche sensible à la casse du cache de requête, à la recherche d'une version préexistante de la requête. Quand il trouve celui du précédent, il utilise simplement ce plan.

L'idée est qu'elle permet d'économiser le temps nécessaire pour décider du plan et le construire. Le trou dans l'idée est lorsque la même requête est exécutée avec des valeurs qui produisent des résultats très différents. Ils devraient avoir des plans différents, mais ils n'en ont pas. Celui qui a exécuté la requête en premier aide à définir le comportement de tous ceux qui l'exécuteront ensuite.

Un exemple rapide: sélectionnez * à partir de [personnes] où lastname = 'SMITH' - nom de famille très populaire aux États-Unis GO sélectionnez * à partir de [personnes] où lastname = 'BONAPARTE' - nom de famille PAS populaire aux États-Unis

Lorsque la requête pour BONAPARTE est exécutée, le plan qui a été créé pour SMITH sera réutilisé. Si SMITH a provoqué une analyse de table (ce qui pourrait être bon , si les lignes dans la table sont à 99% SMITH), alors BONAPARTE obtiendra également une analyse de table. Si BONAPARTE a été exécuté avant SMITH, un plan utilisant un index peut être construit et utilisé, puis utilisé à nouveau pour SMITH (ce qui pourrait être mieux avec l'analyse de table). Les gens peuvent ne pas remarquer que les performances de SMITH sont médiocres, car ils s'attendent à de mauvaises performances, car la table entière doit être lue et la lecture de l'index et le saut vers la table n'est pas directement remarquée.

En ce qui concerne vos changements qui devraient changer quoi que ce soit, je soupçonne que SQL ne considère cela que comme une requête totalement différente et crée un nouveau plan, spécifique à votre valeur de [un certain nombre].

Pour tester cela, apportez une modification absurde à la requête, comme l'ajout de quelques espaces entre FOR et le nom de la table, ou mettez un commentaire à la fin. C'est rapide? Si c'est le cas, c'est parce que cette requête est légèrement différente de celle qui se trouve dans le cache, donc SQL a fait ce qu'elle fait pour les "nouvelles" requêtes.

Pour une solution, j'examinerais trois choses. Tout d'abord, assurez-vous que vos statistiques sont à jour. Cela devrait vraiment être la première chose à faire quand une requête semble bizarre ou aléatoire. Votre DBA devrait le faire, mais des choses se produisent. La manière habituelle d'assurer des statistiques à jour est de réindexer vos tables, ce qui n'est pas nécessairement une chose légère à faire, mais il existe également des options pour simplement mettre à jour les statistiques.

La deuxième chose à penser est d'ajouter des index dans le sens des suggestions de Remus. Avec un indice meilleur / différent, une valeur par rapport à une autre pourrait être plus stable et ne pas varier si énormément.

Si cela n'aide pas, la troisième chose à essayer est de forcer un nouveau plan à chaque fois que vous exécutez l'instruction, à l'aide du mot clé RECOMPILE:

sélectionnez * dans [grand tableau] où serial_number = [un certain nombre] ordre par test_date desc OPTION (RECOMPILE)

Il y a un article décrivant une situation similaire ici . Franchement, je n'avais vu que RECOMPILE appliqué aux procédures stockées auparavant, mais il semble fonctionner avec des instructions SELECT "régulières". Kimberly Tripp ne m'a jamais trompé.

Vous pouvez également examiner la fonctionnalité appelée « guides de plan », mais elle est plus complexe et peut être exagérée.

détroit de darin
la source
Pour répondre à certaines de ces préoccupations: 1. Les statistiques ont été mises à jour, sont en cours de mise à jour. 2. Nous avons essayé l'indexation de plusieurs manières (couvrant les index, etc.) mais le problème semble être plus lié à l' order byutilisation contre un index datetime en particulier. 3. Je viens d'essayer votre idée avec l'option RECOMPILE, elle a toujours échoué, ce qui m'a un peu surpris, j'espérais que ça allait marcher, même si je ne sais pas si c'est une solution pour la production.
DBtheDBA