Sous-requête utilisant Exists 1 ou Exists *

88

J'écrivais mes chèques EXISTS comme ceci:

IF EXISTS (SELECT * FROM TABLE WHERE Columns=@Filters)
BEGIN
   UPDATE TABLE SET ColumnsX=ValuesX WHERE Where Columns=@Filters
END

Un des DBA dans une vie précédente m'a dit que lorsque je fais une EXISTSclause, utilisez SELECT 1au lieu deSELECT *

IF EXISTS (SELECT 1 FROM TABLE WHERE Columns=@Filters)
BEGIN
   UPDATE TABLE SET ColumnsX=ValuesX WHERE Columns=@Filters
END

Cela fait-il vraiment une différence?

Raj Plus
la source
1
Vous avez oublié EXISTS (SELECT NULL FROM ...). Cela a été demandé récemment
OMG Ponies
16
ps obtenir un nouveau DBA. La superstition n'a pas sa place en informatique, en particulier dans la gestion de base de données (d'un ancien DBA !!!)
Matt Rogish

Réponses:

135

Non, SQL Server est intelligent et sait qu'il est utilisé pour un EXISTS et ne renvoie AUCUNE DONNÉE au système.

Quoth Microsoft: http://technet.microsoft.com/en-us/library/ms189259.aspx?ppud=4

La liste de sélection d'une sous-requête introduite par EXISTS se compose presque toujours d'un astérisque (*). Il n'y a aucune raison de lister les noms de colonnes car vous testez simplement si les lignes qui remplissent les conditions spécifiées dans la sous-requête existent.

Pour vous vérifier, essayez d'exécuter ce qui suit:

SELECT whatever
  FROM yourtable
 WHERE EXISTS( SELECT 1/0
                 FROM someothertable 
                WHERE a_valid_clause )

S'il faisait réellement quelque chose avec la liste SELECT, il lèverait une erreur div par zéro. Ce n'est pas le cas.

EDIT: Remarquez que le standard SQL en parle.

Norme ANSI SQL 1992, p. 191 http://www.contrib.andrew.cmu.edu/~shadow/sql/sql1992.txt

3) Cas:
a) Si le <select list>"*" est simplement contenu dans un <subquery> qui est immédiatement contenu dans un <exists predicate>, alors le <select list> est équivalent à un <value expression> qui est un arbitraire <literal>.

Matt Rogish
la source
1
l' EXISTSastuce avec 1/0 peut même être étendue à cela SELECT 1 WHERE EXISTS(SELECT 1/0)... semble un pas plus abstrait que le second SELECTn'a pas de FROMclause
whytheq
1
@whytheq - Ou SELECT COUNT(*) WHERE EXISTS(SELECT 1/0). Un SELECTsans un FROMdans SQL Server est traité comme s'il accédait à une table à une seule ligne (par exemple, similaire à la sélection à partir de la dualtable dans d'autres SGBDR)
Martin Smith
@MartinSmith applaudit - le fait est donc que cela SELECTcrée une table à 1 ligne avant de faire autre chose, même si 1/0la table à 1 ligne est toujours foutue EXISTS?
whytheq
Cela a-t-il toujours été le cas ou s'agit-il d'une optimisation introduite dans une version particulière de SQL Server?
Martin Brown
1
@MartinSmith TIL "quoth". Merci de l'avoir réparé.
Gurwinder Singh
111

La raison de cette idée fausse est probablement due à la croyance que cela finira par lire toutes les colonnes. Il est facile de voir que ce n'est pas le cas.

CREATE TABLE T
(
X INT PRIMARY KEY,
Y INT,
Z CHAR(8000)
)

CREATE NONCLUSTERED INDEX NarrowIndex ON T(Y)

IF EXISTS (SELECT * FROM T)
    PRINT 'Y'

Donne plan

Plan

Cela montre que SQL Server a pu utiliser l'index le plus étroit disponible pour vérifier le résultat malgré le fait que l'index n'inclut pas toutes les colonnes. L'accès à l'index se fait sous un opérateur de semi-jointure, ce qui signifie qu'il peut arrêter l'analyse dès que la première ligne est renvoyée.

Il est donc clair que la croyance ci-dessus est fausse.

Cependant, Conor Cunningham de l'équipe Optimiseur de requête explique ici qu'il utilise généralement SELECT 1dans ce cas, car cela peut faire une légère différence de performances dans la compilation de la requête.

Le QP prendra et développera tous *les éléments au début du pipeline et les liera aux objets (dans ce cas, la liste des colonnes). Il supprimera ensuite les colonnes inutiles en raison de la nature de la requête.

Donc, pour une EXISTSsous-requête simple comme celle-ci:

SELECT col1 FROM MyTable WHERE EXISTS (SELECT * FROM Table2 WHERE MyTable.col1=Table2.col2)Le *sera étendu à une liste de colonnes potentiellement importante, puis il sera déterminé que la sémantique du EXISTSne nécessite aucune de ces colonnes, donc pratiquement toutes peuvent être supprimées.

" SELECT 1" évitera d'avoir à examiner les métadonnées inutiles pour cette table lors de la compilation de la requête.

Cependant, au moment de l'exécution, les deux formes de la requête seront identiques et auront des temps d'exécution identiques.

J'ai testé quatre façons possibles d'exprimer cette requête sur une table vide avec différents nombres de colonnes. SELECT 1vs SELECT *vs SELECT Primary_Keyvs SELECT Other_Not_Null_Column.

J'ai exécuté les requêtes en boucle en utilisant OPTION (RECOMPILE)et mesuré le nombre moyen d'exécutions par seconde. Résultats ci-dessous

entrez la description de l'image ici

+-------------+----------+---------+---------+--------------+
| Num of Cols |    *     |    1    |   PK    | Not Null col |
+-------------+----------+---------+---------+--------------+
| 2           | 2043.5   | 2043.25 | 2073.5  | 2067.5       |
| 4           | 2038.75  | 2041.25 | 2067.5  | 2067.5       |
| 8           | 2015.75  | 2017    | 2059.75 | 2059         |
| 16          | 2005.75  | 2005.25 | 2025.25 | 2035.75      |
| 32          | 1963.25  | 1967.25 | 2001.25 | 1992.75      |
| 64          | 1903     | 1904    | 1936.25 | 1939.75      |
| 128         | 1778.75  | 1779.75 | 1799    | 1806.75      |
| 256         | 1530.75  | 1526.5  | 1542.75 | 1541.25      |
| 512         | 1195     | 1189.75 | 1203.75 | 1198.5       |
| 1024        | 694.75   | 697     | 699     | 699.25       |
+-------------+----------+---------+---------+--------------+
| Total       | 17169.25 | 17171   | 17408   | 17408        |
+-------------+----------+---------+---------+--------------+

Comme on peut le voir, il n'y a pas de gagnant constant entre SELECT 1et SELECT *et la différence entre les deux approches est négligeable. Les SELECT Not Null colet SELECT PKapparaissent cependant légèrement plus rapides.

Les quatre requêtes se dégradent à mesure que le nombre de colonnes de la table augmente.

Comme le tableau est vide, cette relation ne semble explicable que par la quantité de métadonnées de colonne. Car COUNT(1)il est facile de voir que cela est réécrit COUNT(*)à un moment donné du processus par le bas.

SET SHOWPLAN_TEXT ON;

GO

SELECT COUNT(1)
FROM master..spt_values

Ce qui donne le plan suivant

  |--Compute Scalar(DEFINE:([Expr1003]=CONVERT_IMPLICIT(int,[Expr1004],0)))
       |--Stream Aggregate(DEFINE:([Expr1004]=Count(*)))
            |--Index Scan(OBJECT:([master].[dbo].[spt_values].[ix2_spt_values_nu_nc]))

Attacher un débogueur au processus SQL Server et interrompre aléatoirement lors de l'exécution de ce qui suit

DECLARE @V int 

WHILE (1=1)
    SELECT @V=1 WHERE EXISTS (SELECT 1 FROM ##T) OPTION(RECOMPILE)

J'ai trouvé que dans les cas où la table contient 1024 colonnes la plupart du temps, la pile d'appels ressemble à quelque chose comme ci-dessous, indiquant qu'elle passe effectivement une grande partie du temps à charger les métadonnées de la colonne même lorsqu'elle SELECT 1est utilisée (dans le cas où le la table a 1 colonne au hasard, la rupture n'a pas atteint ce bit de la pile d'appels en 10 tentatives)

sqlservr.exe!CMEDAccess::GetProxyBaseIntnl()  - 0x1e2c79 bytes  
sqlservr.exe!CMEDProxyRelation::GetColumn()  + 0x57 bytes   
sqlservr.exe!CAlgTableMetadata::LoadColumns()  + 0x256 bytes    
sqlservr.exe!CAlgTableMetadata::Bind()  + 0x15c bytes   
sqlservr.exe!CRelOp_Get::BindTree()  + 0x98 bytes   
sqlservr.exe!COptExpr::BindTree()  + 0x58 bytes 
sqlservr.exe!CRelOp_FromList::BindTree()  + 0x5c bytes  
sqlservr.exe!COptExpr::BindTree()  + 0x58 bytes 
sqlservr.exe!CRelOp_QuerySpec::BindTree()  + 0xbe bytes 
sqlservr.exe!COptExpr::BindTree()  + 0x58 bytes 
sqlservr.exe!CScaOp_Exists::BindScalarTree()  + 0x72 bytes  
... Lines omitted ...
msvcr80.dll!_threadstartex(void * ptd=0x0031d888)  Line 326 + 0x5 bytes C
kernel32.dll!_BaseThreadStart@8()  + 0x37 bytes 

Cette tentative de profilage manuel est sauvegardée par le profileur de code VS 2012 qui montre une sélection très différente de fonctions consommant le temps de compilation pour les deux cas ( Top 15 Functions 1024 colonnes vs Top 15 Functions 1 colonne ).

Les versions SELECT 1et SELECT *finissent par vérifier les autorisations de colonne et échouent si l'utilisateur n'a pas accès à toutes les colonnes de la table.

Un exemple que j'ai cribbed d'une conversation sur le tas

CREATE USER blat WITHOUT LOGIN;
GO
CREATE TABLE dbo.T
(
X INT PRIMARY KEY,
Y INT,
Z CHAR(8000)
)
GO

GRANT SELECT ON dbo.T TO blat;
DENY SELECT ON dbo.T(Z) TO blat;
GO
EXECUTE AS USER = 'blat';
GO

SELECT 1
WHERE  EXISTS (SELECT 1
               FROM   T); 
/*  ↑↑↑↑ 
Fails unexpectedly with 

The SELECT permission was denied on the column 'Z' of the 
           object 'T', database 'tempdb', schema 'dbo'.*/

GO
REVERT;
DROP USER blat
DROP TABLE T

On pourrait donc supposer que la différence apparente mineure lors de l'utilisation SELECT some_not_null_colest qu'elle ne vérifie que les autorisations sur cette colonne spécifique (bien qu'elle charge toujours les métadonnées pour tous). Cependant, cela ne semble pas correspondre aux faits, car la différence de pourcentage entre les deux approches se réduit si le nombre de colonnes dans le tableau sous-jacent augmente.

Dans tous les cas, je ne me précipiterai pas et ne changerai pas toutes mes requêtes sous cette forme car la différence est très mineure et n'apparaîtra que lors de la compilation des requêtes. La suppression du OPTION (RECOMPILE)afin que les exécutions ultérieures puissent utiliser un plan mis en cache a donné ce qui suit.

entrez la description de l'image ici

+-------------+-----------+------------+-----------+--------------+
| Num of Cols |     *     |     1      |    PK     | Not Null col |
+-------------+-----------+------------+-----------+--------------+
| 2           | 144933.25 | 145292     | 146029.25 | 143973.5     |
| 4           | 146084    | 146633.5   | 146018.75 | 146581.25    |
| 8           | 143145.25 | 144393.25  | 145723.5  | 144790.25    |
| 16          | 145191.75 | 145174     | 144755.5  | 146666.75    |
| 32          | 144624    | 145483.75  | 143531    | 145366.25    |
| 64          | 145459.25 | 146175.75  | 147174.25 | 146622.5     |
| 128         | 145625.75 | 143823.25  | 144132    | 144739.25    |
| 256         | 145380.75 | 147224     | 146203.25 | 147078.75    |
| 512         | 146045    | 145609.25  | 145149.25 | 144335.5     |
| 1024        | 148280    | 148076     | 145593.25 | 146534.75    |
+-------------+-----------+------------+-----------+--------------+
| Total       | 1454769   | 1457884.75 | 1454310   | 1456688.75   |
+-------------+-----------+------------+-----------+--------------+

Le script de test que j'ai utilisé peut être trouvé ici

Martin Smith
la source
3
+1 Cette réponse mérite plus de votes pour l'effort impliqué pour obtenir des données réelles.
Jon
1
Une idée de la version de SQL Server sur laquelle ces statistiques ont été générées?
Martin Brown
3
@MartinBrown - IIRC à l'origine en 2008 bien que j'ai refait les tests récemment en 2012 pour la modification la plus récente et ai trouvé la même chose.
Martin Smith
8

Le meilleur moyen de le savoir est de tester les performances des deux versions et de consulter le plan d'exécution des deux versions. Choisissez une table avec beaucoup de colonnes.

HLGEM
la source
2
+1. Je ne sais pas pourquoi cela a été voté à la baisse. J'ai toujours pensé qu'il valait mieux apprendre à un homme à pêcher que de simplement lui donner un poisson. Comment les gens vont-ils apprendre quelque chose?
Ogre Psalm33
5

Il n'y a aucune différence dans SQL Server et cela n'a jamais posé de problème dans SQL Server. L'optimiseur sait qu'ils sont identiques. Si vous regardez les plans d'exécution, vous verrez qu'ils sont identiques.

Cade Roux
la source
1

Personnellement, je trouve très, très difficile de croire qu'ils n'optimisent pas le même plan de requête. Mais la seule façon de savoir dans votre situation particulière est de la tester. Si vous le faites, veuillez faire un rapport!

Larry Lustig
la source
-1

Pas de réelle différence mais il pourrait y avoir un très petit impact sur les performances. En règle générale, vous ne devriez pas demander plus de données que ce dont vous avez besoin.

Orjan
la source