Причина этого заблуждения, по-видимому, связана с убеждением, что в конечном итоге он прочитает все столбцы. Легко понять, что это не так.
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'
Дает план
Это показывает, что SQL Server смог использовать самый узкий доступный индекс для проверки результата, несмотря на то, что индекс не включает все столбцы. Доступ к индексу осуществляется с помощью оператора полусоединения, что означает, что он может остановить сканирование, как только будет возвращена первая строка.
Итак, ясно, что это мнение ошибочно.
Однако Конор Каннингем из группы оптимизатора запросов объясняет здесь, что он обычно использует SELECT 1
в этом случае, поскольку это может незначительно повлиять на производительность при компиляции запроса.
QP возьмет и развернет все в *
начале конвейера и привяжет их к объектам (в данном случае к списку столбцов). Затем он удалит ненужные столбцы из-за характера запроса.
Итак, для простого EXISTS
подзапроса, подобного этому:
SELECT col1 FROM MyTable WHERE EXISTS
(SELECT * FROM Table2 WHERE
MyTable.col1=Table2.col2)
*
Будет расширена до некоторой потенциально большой список столбцов , а затем будет определено , что семантика
EXISTS
не требует какой - либо из этих столбцов, поэтому в основном все они могут быть удалены.
" SELECT 1
" позволит избежать проверки любых ненужных метаданных для этой таблицы во время компиляции запроса.
Однако во время выполнения две формы запроса будут идентичны и будут иметь одинаковое время выполнения.
Я протестировал четыре возможных способа выражения этого запроса на пустой таблице с различным количеством столбцов. SELECT 1
против SELECT *
против SELECT Primary_Key
против SELECT Other_Not_Null_Column
.
Я выполнял запросы в цикле, используя OPTION (RECOMPILE)
и измеряя среднее количество выполнений в секунду. Результаты ниже
+
| 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 |
+
Как можно видеть, нет последовательного победителя между SELECT 1
и, SELECT *
а разница между двумя подходами незначительна. SELECT Not Null col
И SELECT PK
появляются немного быстрее , хотя.
Производительность всех четырех запросов снижается по мере увеличения количества столбцов в таблице.
Поскольку таблица пуста, эта связь кажется объяснимой только количеством метаданных столбца. Поскольку COUNT(1)
легко увидеть, что это COUNT(*)
в какой-то момент переписывается из приведенного ниже.
SET SHOWPLAN_TEXT ON;
GO
SELECT COUNT(1)
FROM master..spt_values
Что дает следующий план
|
|
|
Присоединение отладчика к процессу SQL Server и случайное прерывание при выполнении нижеприведенного
DECLARE @V int
WHILE (1=1)
SELECT @V=1 WHERE EXISTS (SELECT 1 FROM
Я обнаружил, что в тех случаях, когда таблица имеет 1024 столбца большую часть времени, стек вызовов выглядит примерно так, как показано ниже, что указывает на то, что он действительно тратит большую часть времени на загрузку метаданных столбца, даже когда SELECT 1
он используется (для случая, когда таблица имеет 1 столбец, который случайным образом разбивается, не попал в этот бит стека вызовов за 10 попыток)
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
Эта попытка ручного профилирования поддерживается профилировщиком кода VS 2012, который показывает совсем другой набор функций, потребляющих время компиляции, для двух случаев ( первые 15 функций - 1024 столбца против лучших 15 функций - 1 столбец ).
Обе версии SELECT 1
и SELECT *
версии завершают проверку разрешений столбцов и терпят неудачу, если пользователю не предоставлен доступ ко всем столбцам в таблице.
Пример я скопировал из разговора на кучу
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
Таким образом, можно предположить, что незначительная очевидная разница при использовании SELECT some_not_null_col
заключается в том, что он завершает проверку разрешений только для этого конкретного столбца (хотя по-прежнему загружает метаданные для всех). Однако это, похоже, не согласуется с фактами, поскольку процентная разница между двумя подходами, если что-то становится меньше по мере увеличения количества столбцов в базовой таблице.
В любом случае я не буду спешить и менять все свои запросы на эту форму, поскольку разница очень незначительна и проявляется только во время компиляции запроса. Удаление, OPTION (RECOMPILE)
чтобы последующие исполнения могли использовать кэшированный план, дало следующее.
+
| 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 |
+
Тестовый сценарий, который я использовал, можно найти здесь