Nous utilisons SQL Server 2008 R2 et avons une très grande table (100M + lignes) avec un index d'ID principal et une datetime
colonne avec un index non cluster. Nous constatons un comportement client / serveur très inhabituel basé sur l'utilisation d'une order by
clause 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 datetime
type), 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_cursorprepexec
procédure de la cause complète du problème. Ajoutez à cela le fait que le sp_cursorprepexec
est é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 forceseek
en 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:
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.
la source
select id, test_date from [big table] where serial_number = ..... order by test_date
- je me demande simplement si celaSELECT *
a un impact négatif sur vos performances. Si vous avez un index non clusterisétest_date
et 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 rapidementsp_executesql
et voyez ce qui se passe.Réponses:
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.
la source
the order
clause? Le plan ne doit-il pas se limiter auxwhere
conditions, 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?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.
la source
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.
la source
order by
utilisation 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.