2009-10-20 4 views
73

Je l'habitude d'écrire mes Exists comme ceci:des sous-requêtes avec exists 1 ou Exists *

IF EXISTS (SELECT * FROM TABLE WHERE [email protected]) 
BEGIN 
    UPDATE TABLE SET ColumnsX=ValuesX WHERE Where [email protected] 
END 

L'une des années DBA dans une vie antérieure m'a dit que quand je fais une clause EXISTS, utilisez SELECT 1 au lieu de SELECT *

IF EXISTS (SELECT 1 FROM TABLE WHERE [email protected]) 
BEGIN 
    UPDATE TABLE SET ColumnsX=ValuesX WHERE [email protected] 
END 

Est-ce que cela fait vraiment une différence?

+1

Vous avez oublié EXISTS (SELECT NULL FROM ...). Cela a été demandé récemment btw –

+13

p.s. obtenir un nouveau DBA. La superstition n'a pas sa place dans l'informatique, surtout en gestion de base de données (d'un ancien DBA !!!) –

Répondre

110

Non. Cela a été couvert à plusieurs reprises. SQL Server est intelligent et sait qu'il est utilisé pour un EXISTS et renvoie NO DATA au système.

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

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

Aussi, ne me croyez pas? Essayez d'exécuter les opérations suivantes:

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 lancerait une erreur div par zéro. Ce n'est pas le cas.

EDIT: Notez que le standard SQL en parle.

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

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

+56

+1 pour 1/0 dans la clause EXISTS. – gbn

+1

le tour 'EXISTS' avec 1/0 peut même être étendu à' SELECT 1 WHERE EXISTS (SELECT 1/0) '... semble un pas plus abstrait alors que le second' SELECT' n'a pas de clause FROM FROM – whytheq

+1

@whytheq - Ou 'SELECT COUNT (*) O EX EXISTE (SELECT 1/0)'. Un 'SELECT 'sans un' FROM' dans SQL Server est traité comme s'il accédait à une seule table de lignes (par ex.similaire à la sélection de la table 'dual' dans d'autres SGBDR) –

7

La meilleure façon de savoir est de tester les performances des deux versions et de vérifier le plan d'exécution pour les deux versions. Choisissez une table avec beaucoup de colonnes.

+2

+1. Aucune idée pourquoi cela a été voté. J'ai toujours pensé qu'il valait mieux apprendre à un homme à pêcher que de lui donner un poisson. Comment les gens vont-ils apprendre quelque chose? –

+1

+1 Faites-le vous-même est toujours le bienvenu. –

-1

Pas vraiment de différence, mais les performances peuvent être très faibles. En règle générale, vous ne devriez pas demander plus de données que nécessaire.

1

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

5

Il n'y a aucune différence dans SQL Server et cela n'a jamais été un 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.

89

La raison de cette idée fausse est probablement due à la croyance qu'elle 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' 

Plan Donne

Plan

Cela montre que SQL Server a été en mesure d'utiliser l'indice le plus étroit disponible pour vérifier le résultat en dépit du fait que l'indice ne comprend pas toutes les colonnes. L'accès à l'index est 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êtes explique here qu'il utilise généralement SELECT 1 dans ce cas, car il peut faire une petite différence de performance dans la compilation de la requête.

La QP prendra et développer tous au début de l » * dans le pipeline et les lient à objets (dans ce cas, la liste des colonnes). Il supprimera ensuite des colonnes inutiles en raison de la nature de la requête .

Donc, pour un simple sous-requête EXISTS comme ceci:

SELECT col1 FROM MyTable WHERE EXISTS (SELECT * FROM Table2 WHERE MyTable.col1=Table2.col2) Le * sera élargi à certains potentiellement grande liste des colonnes puis il sera déterminé que la sémantique du EXISTS ne nécessite pas de ces colonnes, donc fondamentalement tous peuvent être supprimés . "" évitera d'avoir à examiner toutes les métadonnées inutiles pour cette table lors de la compilation de la requête.

Cependant, lors de l'exécution, les deux formes 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 1 contre SELECT * contre SELECT Primary_Key contre SELECT Other_Not_Null_Column.

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

enter image description here

+-------------+----------+---------+---------+--------------+ 
| 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 le voit, il n'y a pas de gagnant cohérent entre SELECT 1 et SELECT * et la différence entre les deux approches est négligeable. Les SELECT Not Null col et SELECT PK semblent légèrement plus rapides.

Les quatre requêtes se dégradent en performance à mesure que le nombre de colonnes dans la table augmente.Comme la table est vide, cette relation semble uniquement explicable par la quantité de métadonnées de colonne. Pour COUNT(1) il est facile de voir que cela est réécrit à COUNT(*) à un moment donné dans le processus de la ci-dessous.

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])) 

Fixation d'un débogueur au processus SQL Server et rupture au hasard tout en exécutant le dessous

DECLARE @V int 

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

Je trouve que dans les cas où la table a 1.024 colonnes la plupart du temps, la pile d'appels ressemble à quelque chose comme ci-dessous, indiquant qu'elle passe en effet une grande partie du temps à charger les métadonnées des colonnes, même si n SELECT 1 est utilisé (pour le cas où la table a une colonne de rupture au hasard n'a pas frappé 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 
[email protected]() + 0x37 bytes 

Ce manuel profiler tentative est sauvegardé par le code VS 2012 profileur ce qui montre une sélection très différente de fonctions consommant le temps de compilation pour les deux cas (Top 15 Functions 1024 columns vs Top 15 Functions 1 column).

Les versions SELECT 1 et 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 je cribbed d'une conversation sur the heap

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 

Donc on peut supposer que la différence apparente mineure lors de l'utilisation SELECT some_not_null_col est qu'il serpente seulement jusqu'à la vérification des autorisations sur cette colonne spécifique (bien que charge encore les métadonnées pour tous). Cependant, cela ne semble pas correspondre aux faits en tant que différence de pourcentage entre les deux approches si quelque chose devient plus petit à mesure que le nombre de colonnes dans le tableau sous-jacent augmente.

Dans tous les cas, je ne vais pas me précipiter et changer toutes mes requêtes à cette forme que la différence est très mineure et seulement apparente lors de la compilation de la requête. Suppression du OPTION (RECOMPILE) afin que les exécutions ultérieures puissent utiliser un plan mis en cache a donné ce qui suit.

enter image description here

+-------------+-----------+------------+-----------+--------------+ 
| 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 | 
+-------------+-----------+------------+-----------+--------------+ 

The test script I used can be found here

+1

+1 Cette réponse mérite plus de votes pour l'effort nécessaire 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? –

+1

@MartinBrown - IIRC à l'origine 2008 bien que j'ai refait les tests récemment sur 2012 pour l'édition la plus récente et ai trouvé la même chose. –

Questions connexes