Причина такої помилкової думки, мабуть, полягає у вірі, що вона в підсумку прочитає всі колонки. Неважко зрозуміти, що це не так.
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, який показує дуже різний набір функцій, що споживає час компіляції для двох випадків ( Top 15 Функції 1024 стовпці проти Top 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 |
+
Скрипт тесту, який я використовував, можна знайти тут