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
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 1
dans 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 EXISTS
sous-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
EXISTS
ne 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 1
vs SELECT *
vs SELECT Primary_Key
vs 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
+-------------+----------+---------+---------+--------------+
| 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 1
et SELECT *
et la différence entre les deux approches est négligeable. Les SELECT Not Null col
et SELECT PK
apparaissent 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 1
est 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 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 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_col
est 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.
+-------------+-----------+------------+-----------+--------------+
| 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