Підзапит із використанням "Існує 1" або "Існує *"


89

Раніше я писав свої чеки EXISTS так:

IF EXISTS (SELECT * FROM TABLE WHERE Columns=@Filters)
BEGIN
   UPDATE TABLE SET ColumnsX=ValuesX WHERE Where Columns=@Filters
END

Один із DBA у попередньому житті сказав мені, що коли я роблю EXISTSстаттю, використовуй SELECT 1замістьSELECT *

IF EXISTS (SELECT 1 FROM TABLE WHERE Columns=@Filters)
BEGIN
   UPDATE TABLE SET ColumnsX=ValuesX WHERE Columns=@Filters
END

Чи насправді це має значення?


1
Ви забули ІСНУЄ (ВИБЕРИТИ НУЛЬ З ...). Це нещодавно запитали btw
OMG Ponies

17
ps отримати новий DBA. Забобонам не місце в ІТ, особливо в управлінні базами даних (від колишнього DBA !!!)
Метт Рогіш,

Відповіді:


136

Ні, SQL Server розумний і знає, що він використовується для ІСНУЄ, і не повертає системі НІЯКИХ ДАНИХ.

Майкрософт: http://technet.microsoft.com/en-us/library/ms189259.aspx?ppud=4

Список вибору підзапиту, введений функцією EXISTS, майже завжди складається із зірочки (*). Немає причин перераховувати імена стовпців, оскільки ви просто перевіряєте, чи існують рядки, які відповідають умовам, зазначеним у підзапиті.

Щоб перевірити себе, спробуйте запустити наступне:

SELECT whatever
  FROM yourtable
 WHERE EXISTS( SELECT 1/0
                 FROM someothertable 
                WHERE a_valid_clause )

Якби він насправді щось робив зі списком SELECT, він видав би div з нульовою помилкою. Це не так.

EDIT: Зверніть увагу, що SQL Standard насправді говорить про це.

Стандарт ANSI SQL 1992, стор. 191 http://www.contrib.andrew.cmu.edu/~shadow/sql/sql1992.txt

3) Випадок:
а) Якщо <select list>"*" просто міститься в a, <subquery> який відразу міститься в an <exists predicate>, тоді <select list> еквівалент a, <value expression> який є довільним <literal>.


1
EXISTSтрюк з 1/0 може бути навіть розширено на це SELECT 1 WHERE EXISTS(SELECT 1/0)... здається , крок більш абстрактно , то , як другий SELECTне має FROMпункту
whytheq

1
@whytheq - Або SELECT COUNT(*) WHERE EXISTS(SELECT 1/0). SELECTБез FROMв SQL Server розглядається як якщо б це був доступ до таблиці однорядні (наприклад , схожий на вибір з dualтаблиці в інший РСУБД)
Мартін Сміт

@MartinSmith вітає - отже, справа в тому, що SELECTстворюється таблиця в 1 рядок, перш ніж вона робить щось інше, навіть незважаючи на те 1/0, що сміття в таблиці з 1 рядком все ще EXISTS?
whytheq

Чи завжди це було так, чи це оптимізація, яка була введена в певній версії SQL Server?
Мартін Браун

1
@MartinSmith TIL "quoth". Дякуємо, що виправили це назад.
Гурвіндер Сінгх,

113

Причина такої помилкової думки, мабуть, полягає у вірі, що вона в підсумку прочитає всі колонки. Неважко зрозуміти, що це не так.

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

Що дає наступний план

  |--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]))

Підключення налагоджувача до процесу SQL Server та випадкове злам при виконанні наведених нижче дій

DECLARE @V int 

WHILE (1=1)
    SELECT @V=1 WHERE EXISTS (SELECT 1 FROM ##T) OPTION(RECOMPILE)

Я виявив, що у випадках, коли таблиця має 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); 
/*  ↑↑↑↑ 
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

Тож можна припустити, що незначна очевидна різниця при використанні 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   |
+-------------+-----------+------------+-----------+--------------+

Скрипт тесту, який я використовував, можна знайти тут


3
+1 Ця відповідь заслуговує на більшу кількість голосів за зусилля, спрямовані на отримання реальних даних.
Джон

1
Будь-яка ідея, на якій версії SQL Server була створена ця статистика?
Мартін Браун

3
@MartinBrown - IIRC спочатку 2008 року, хоча нещодавно в 2012 році я переробив тести для останнього редагування і знайшов те саме.
Martin Smith

8

Найкращий спосіб дізнатись - перевірити продуктивність обох версій і перевірити план виконання обох версій. Підберіть таблицю з великою кількістю стовпців.


2
+1. Не знаю, чому за це було проголосовано. Я завжди думав, що краще навчити чоловіка ловити рибу, ніж просто давати йому рибу. Як люди збираються чомусь навчитися?
Огре Псалом 33,

5

У SQL Server немає різниці, і в SQL Server це ніколи не було проблемою. Оптимізатор знає, що вони однакові. Якщо ви подивитесь на плани виконання, то побачите, що вони ідентичні.


1

Особисто мені дуже, дуже важко повірити, що вони не оптимізуються до одного плану запитів. Але єдиний спосіб дізнатись у вашій конкретній ситуації - це перевірити це. Якщо ви це зробите, повідомте про це ще раз!


-1

Не якась реальна різниця, але може бути дуже невеликий показник продуктивності. Як правило, ви не повинні запитувати більше даних, ніж вам потрібно.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.