Przyczyną tego błędnego przekonania jest przypuszczalnie przekonanie, że przeczyta on wszystkie kolumny. Łatwo zauważyć, że tak nie jest.
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'
Przedstawia plan
Pokazuje to, że SQL Server był w stanie użyć najwęższego dostępnego indeksu do sprawdzenia wyniku, mimo że indeks nie obejmuje wszystkich kolumn. Dostęp do indeksu odbywa się pod operatorem półłączenia, co oznacza, że może zatrzymać skanowanie, gdy tylko zostanie zwrócony pierwszy wiersz.
Jest więc jasne, że powyższe przekonanie jest błędne.
Jednak Conor Cunningham z zespołu Query Optimiser wyjaśnia tutaj , że zazwyczaj używa SELECT 1
w tym przypadku, ponieważ może to spowodować niewielką różnicę w wydajności podczas kompilacji zapytania.
QP weźmie i rozszerzy wszystko na *
wczesnym etapie potoku i powiąże je z obiektami (w tym przypadku z listą kolumn). Następnie usunie niepotrzebne kolumny ze względu na charakter zapytania.
Więc dla prostego EXISTS
podzapytania, takiego jak to:
SELECT col1 FROM MyTable WHERE EXISTS
(SELECT * FROM Table2 WHERE
MyTable.col1=Table2.col2)
*
Zostanie poszerzona do pewnego potencjalnie dużym liście kolumn, a następnie zostanie ustalone, że semantykę
EXISTS
nie wymaga żadnej z tych kolumn, więc w zasadzie wszystkie z nich mogą być usunięte.
„ SELECT 1
” pozwoli uniknąć sprawdzania niepotrzebnych metadanych dla tej tabeli podczas kompilacji zapytania.
Jednak w czasie wykonywania obie formy zapytania będą identyczne i będą miały identyczne środowiska wykonawcze.
Przetestowałem cztery możliwe sposoby wyrażenia tego zapytania na pustej tabeli z różną liczbą kolumn. SELECT 1
vs SELECT *
vs SELECT Primary_Key
vs SELECT Other_Not_Null_Column
.
Uruchomiłem zapytania w pętli, używając OPTION (RECOMPILE)
i zmierzyłem średnią liczbę wykonań na sekundę. Wyniki poniżej
+
| 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 |
+
Jak widać, nie ma stałego zwycięzcy między SELECT 1
i, SELECT *
a różnica między tymi dwoma podejściami jest znikoma. Jednak SELECT Not Null col
i SELECT PK
pojawiają się nieco szybciej.
Wydajność wszystkich czterech zapytań spada wraz ze wzrostem liczby kolumn w tabeli.
Ponieważ tabela jest pusta, relacja ta wydaje się możliwa do wyjaśnienia jedynie ilością metadanych kolumny. Za COUNT(1)
to łatwo zauważyć, że ten zostanie przepisany do COUNT(*)
w pewnym momencie w procesie od poniżej.
SET SHOWPLAN_TEXT ON;
GO
SELECT COUNT(1)
FROM master..spt_values
Co daje następujący plan
|
|
|
Dołączanie debugera do procesu SQL Server i losowe przerywanie podczas wykonywania poniższych czynności
DECLARE @V int
WHILE (1=1)
SELECT @V=1 WHERE EXISTS (SELECT 1 FROM
Zauważyłem, że w przypadkach, w których tabela ma 1024 kolumny przez większość czasu, stos wywołań wygląda jak poniżej, wskazując, że rzeczywiście spędza dużą część czasu na ładowaniu metadanych kolumn, nawet gdy SELECT 1
jest używany (w przypadku tabela ma 1 kolumnę, losowo łamana, nie trafiła tego bitu stosu wywołań w 10 próbach)
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
Ta próba ręcznego profilowania jest obsługiwana przez profiler kodu VS 2012, który pokazuje bardzo różny wybór funkcji zużywających czas kompilacji dla dwóch przypadków ( 15 najważniejszych funkcji, 1024 kolumn w porównaniu z 15 najpopularniejszymi funkcjami 1 kolumna ).
Obie wersje SELECT 1
i SELECT *
kończą sprawdzanie uprawnień do kolumn i kończą się niepowodzeniem, jeśli użytkownikowi nie udzielono dostępu do wszystkich kolumn w tabeli.
Przykład, który wyciągnąłem z rozmowy na stercie
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);
GO
REVERT;
DROP USER blat
DROP TABLE T
Można więc spekulować, że niewielka widoczna różnica podczas używania SELECT some_not_null_col
polega na tym, że kończy sprawdzanie uprawnień tylko do tej konkretnej kolumny (chociaż nadal ładuje metadane dla wszystkich). Jednak wydaje się, że nie pasuje to do faktów, ponieważ procentowa różnica między tymi dwoma podejściami, jeśli cokolwiek zmniejsza się, gdy wzrasta liczba kolumn w tabeli bazowej.
W każdym razie nie będę się spieszyć i zmieniać wszystkich moich zapytań do tego formularza, ponieważ różnica jest bardzo niewielka i widoczna tylko podczas kompilacji zapytań. Usunięcie OPTION (RECOMPILE)
planu, aby kolejne wykonania mogły korzystać z planu buforowanego, dało następujące efekty.
+
| 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 |
+
Skrypt testowy, którego użyłem, można znaleźć tutaj