TL;DR : ответ Caius Jard является правильным - вы можете присоединиться к чему угодно, если оно имеет значение true или false (игнорируя неизвестное).
К сожалению, способ объединения двух таблиц может иметьрезко отличается производительность в зависимости от вашей методологии. Если вы присоединитесь к выражению, вы, как правило, получите очень низкую производительность. Использование вычисляемых столбцов, материализация промежуточного результата в таблице или разбиение ваших условий объединения могут помочь с низкой производительностью.
Объединения - не единственное место, где выражения могут вас окрасить;группировка, агрегирование, фильтры или все, что основано на хорошей оценке количества элементов, пострадает при использовании выражений.
Когда я сравниваю два метода объединения (они функционально эквивалентны, несмотря на новый магический столбец; большеоб этом позже)
SELECT *
FROM #Build AppBuild
LEFT OUTER JOIN #Job Job
ON ( AppBuild.Name = Job.DATA_PROJECT_NAME
AND Job.DATA_PROJECT_NAME NOT LIKE 'BTG -%' )
OR ( Job.DATA_PROJECT_NAME LIKE 'BTG -%'
AND Job.JOB_DESCRIPTION = AppBuild.Name );
SELECT *
FROM #Build AppBuild
LEFT OUTER JOIN #Job Job
ON AppBuild.Name = Job.JoinOnMe;
Результирующие планы запросов имеют огромные различия:
Вы заметите, что ориентировочная стоимостьпервого соединения намного выше - но это даже не рассказывает всей истории. Если я на самом деле выполняю эти два запроса с ~ 6M строками в каждой таблице, я получаю второй, заканчивающий ~ 7 секунд, а первый почти не выполняется через 2 минуты. Это связано с тем, что предикат объединения был помещен в таблицу сканирования таблицы #Job
:
SQL Server не знает, какой процент записей будет иметьDATA_PROJECT_NAME (NOT) LIKE 'BTG -%'
, поэтому он выбирает оценку в 1 строку. Затем это приводит к выбору соединения с вложенным циклом, а также к сортировке и катушке, которые все в конечном итоге приводят к тому, что все работает довольно плохо для нас, когда мы получаем намного больше, чем 1 строку из этого сканирования таблицы.
Исправление? Вычисляемые столбцы. Я создал свои таблицы следующим образом:
CREATE TABLE #Build
(
Name varchar(50) COLLATE DATABASE_DEFAULT NOT NULL
);
CREATE TABLE #Job
(
JOB_DESCRIPTION varchar(50) COLLATE DATABASE_DEFAULT NOT NULL,
DATA_PROJECT_NAME varchar(50) COLLATE DATABASE_DEFAULT NOT NULL,
JoinOnMe AS CASE WHEN DATA_PROJECT_NAME LIKE N'BTG -%' THEN DATA_PROJECT_NAME
ELSE JOB_DESCRIPTION END
);
Оказывается, SQL Server будет вести статистику по JoinOnMe
, даже если внутри него есть выражение, и это значение нигде не материализовалось. Если бы вы захотели, вы могли бы даже индексировать вычисляемый столбец.
Поскольку у нас есть статистика по JoinOnMe
, объединение по ней даст хорошую оценку количества элементов (когда я проверял это было точно), и, таким образом,хороший план.
Если у вас нет свободы изменять таблицу, то вы должны хотя бы разделить объединение на два объединения. Это может показаться нелогичным, но если вы объединяете множество условий для внешнего объединения, SQL Server обычно получает более точную оценку (и, следовательно, лучшие планы), если каждое условие OR
является отдельным, и вызатем COALESCE
набор результатов.
Когда я включаю запрос, подобный этому:
SELECT AppBuild.Name,
COALESCE( Job.JOB_DESCRIPTION, Job2.JOB_DESCRIPTION ) JOB_DESCRIPTION,
COALESCE( Job.DATA_PROJECT_NAME, Job2.DATA_PROJECT_NAME ) DATA_PROJECT_NAME
FROM #Build AppBuild
LEFT OUTER JOIN #Job Job
ON ( AppBuild.Name = Job.DATA_PROJECT_NAME
AND Job.DATA_PROJECT_NAME NOT LIKE 'BTG -%' )
LEFT OUTER JOIN #Job Job2
ON ( Job2.DATA_PROJECT_NAME LIKE 'BTG -%'
AND Job2.JOB_DESCRIPTION = AppBuild.Name );
Это также 0% от общей стоимости относительно первого запроса. При сравнении с объединением в вычисляемом столбце разница составляет около 58% / 42%
Вот как я создал таблицы инаполнил их данными испытаний
DROP TABLE IF EXISTS #Build;
DROP TABLE IF EXISTS #Job;
CREATE TABLE #Build
(
Name varchar(50) COLLATE DATABASE_DEFAULT NOT NULL
);
CREATE TABLE #Job
(
JOB_DESCRIPTION varchar(50) COLLATE DATABASE_DEFAULT NOT NULL,
DATA_PROJECT_NAME varchar(50) COLLATE DATABASE_DEFAULT NOT NULL,
JoinOnMe AS CASE WHEN DATA_PROJECT_NAME LIKE N'BTG -%' THEN DATA_PROJECT_NAME
ELSE JOB_DESCRIPTION END
);
INSERT INTO #Build
( Name )
SELECT ROW_NUMBER() OVER ( ORDER BY ( SELECT NULL ))
FROM master.dbo.spt_values
CROSS APPLY master.dbo.spt_values SV2;
INSERT INTO #Job
( JOB_DESCRIPTION, DATA_PROJECT_NAME )
SELECT ROW_NUMBER() OVER ( ORDER BY ( SELECT NULL )),
CASE WHEN ROUND( RAND(), 0 ) = 1 THEN CAST(ROW_NUMBER() OVER ( ORDER BY ( SELECT NULL )) AS nvarchar(20))
ELSE 'BTG -1' END
FROM master.dbo.spt_values SV
CROSS APPLY master.dbo.spt_values SV2;