Подзапрос с использованием Exists 1 или Exists *

89

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

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

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

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

Это действительно имеет значение?

Радж Мор
источник
1
Вы забыли EXISTS (SELECT NULL FROM ...). Это было недавно спросили , кстати
OMG Ponies
17
ps получите нового администратора базы данных. Суевериям нет места в ИТ, особенно в управлении базами данных (от бывшего администратора баз данных !!!)
Мэтт Рогиш

Ответы:

136

Нет, SQL Server умен и знает, что он используется для EXISTS, и не возвращает системе НИКАКИХ ДАННЫХ.

Ответ Microsoft: 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 с нулевой ошибкой. Это не так.

РЕДАКТИРОВАТЬ: Обратите внимание, что стандарт SQL действительно говорит об этом.

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

3) Случай:
a) Если <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). A SELECTбез a FROMв SQL Server обрабатывается так, как если бы он обращался к таблице с одной строкой (например, аналогично выбору из dualтаблицы в других СУБД)
Мартин Смит
@MartinSmith приветствует - так что дело в том, что SELECTсоздается таблица с 1 строкой, прежде чем она сделает что-либо еще, так что даже если 1/0таблица с 1 строкой все еще мусор EXISTS?
whytheq
Так было всегда или это оптимизация, которая была введена в конкретной версии SQL Server?
Мартин Браун
1
@MartinSmith TIL "молчал". Спасибо, что исправили его.
Гурвиндер Сингх
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, который показывает совсем другой набор функций, потребляющих время компиляции, для двух случаев ( первые 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); 
/*  ↑↑↑↑ 
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 года для самого последнего редактирования и нашел то же самое.
Мартин Смит
8

Лучший способ узнать - проверить производительность обеих версий и проверить план выполнения для обеих версий. Выберите таблицу с большим количеством столбцов.

HLGEM
источник
2
+1. Понятия не имею, почему это было отклонено. Я всегда считал, что лучше научить человека ловить рыбу, чем просто дать ему рыбу. Как люди собираются чему-нибудь научиться?
Ogre Psalm33
5

В SQL Server нет разницы, и в SQL Server это никогда не было проблемой. Оптимизатор знает, что они одинаковы. Если вы посмотрите на планы выполнения, вы увидите, что они идентичны.

Кейд Ру
источник
1

Лично мне очень и очень трудно поверить, что они не оптимизируются под один и тот же план запроса. Но единственный способ узнать это в вашей конкретной ситуации - это протестировать. Если да, сообщите об этом!

Ларри Люстиг
источник
-1

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

Orjan
источник