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
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
+-------------+----------+---------+---------+--------------+
| 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.
+-------------+-----------+------------+-----------+--------------+
| 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
Vous avez oublié EXISTS (SELECT NULL FROM ...). Cela a été demandé récemment btw –
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 !!!) –