INNER JOIN - продуктивність LEFT JOIN у SQL Server


259

Я створив команду SQL, яка використовує INNER JOIN на 9 таблицях, у будь-якому випадку ця команда займає дуже багато часу (більше п'яти хвилин). Тож мій фольклор запропонував мені змінити INNER JOIN на LEFT JOIN, оскільки ефективність роботи LEFT JOIN краща, незважаючи на те, що я знаю. Після того як я змінив її, швидкість запиту значно покращилася.

Мені хотілося б дізнатися, чому СПІЛЬНЕ ПРИЄДНАННЯ швидше, ніж ВНУТРІШНЯ ПРИЄДНАЙТЕСЬ?

Моя команда SQL виглядає нижче: SELECT * FROM A INNER JOIN B ON ... INNER JOIN C ON ... INNER JOIN Dі так далі

Оновлення: Це коротка моя схема.

FROM sidisaleshdrmly a -- NOT HAVE PK AND FK
    INNER JOIN sidisalesdetmly b -- THIS TABLE ALSO HAVE NO PK AND FK
        ON a.CompanyCd = b.CompanyCd 
           AND a.SPRNo = b.SPRNo 
           AND a.SuffixNo = b.SuffixNo 
           AND a.dnno = b.dnno
    INNER JOIN exFSlipDet h -- PK = CompanyCd, FSlipNo, FSlipSuffix, FSlipLine
        ON a.CompanyCd = h.CompanyCd
           AND a.sprno = h.AcctSPRNo
    INNER JOIN exFSlipHdr c -- PK = CompanyCd, FSlipNo, FSlipSuffix
        ON c.CompanyCd = h.CompanyCd
           AND c.FSlipNo = h.FSlipNo 
           AND c.FSlipSuffix = h.FSlipSuffix 
    INNER JOIN coMappingExpParty d -- NO PK AND FK
        ON c.CompanyCd = d.CompanyCd
           AND c.CountryCd = d.CountryCd 
    INNER JOIN coProduct e -- PK = CompanyCd, ProductSalesCd
        ON b.CompanyCd = e.CompanyCd
           AND b.ProductSalesCd = e.ProductSalesCd 
    LEFT JOIN coUOM i -- PK = UOMId
        ON h.UOMId = i.UOMId 
    INNER JOIN coProductOldInformation j -- PK = CompanyCd, BFStatus, SpecCd
        ON a.CompanyCd = j.CompanyCd
            AND b.BFStatus = j.BFStatus
            AND b.ProductSalesCd = j.ProductSalesCd
    INNER JOIN coProductGroup1 g1 -- PK = CompanyCd, ProductCategoryCd, UsedDepartment, ProductGroup1Cd
        ON e.ProductGroup1Cd  = g1.ProductGroup1Cd
    INNER JOIN coProductGroup2 g2 -- PK = CompanyCd, ProductCategoryCd, UsedDepartment, ProductGroup2Cd
        ON e.ProductGroup1Cd  = g2.ProductGroup1Cd

1
Чи проектуєте ви якийсь атрибут coUOM? Якщо ні, то, можливо, ви зможете використовувати напівз'єднання. Якщо так, ви могли б використовувати UNIONяк альтернативу. Тут FROMрозміщується лише ваша стаття - це недостатня інформація.
одного дня, коли

1
Я так часто замислювався над цим (бо бачу весь час).
Пол Дрейпер

1
Ви пропустили замовлення у своїй короткій схемі? Нещодавно я стикався з проблемою, коли зміна ВНУТРІШНЬОГО ПРИЄДНУВАННЯ на ЛІВНІЙ ВИХІД ПРИЄДНУЙСЯ пришвидшує запит від 3 хвилин до 10 секунд. Якщо ви дійсно маєте Order By у своєму запиті, я поясню далі як відповідь. Схоже, всі відповіді насправді не пояснювали випадку, з яким я стикався.
Phuah Yee Keat

Відповіді:


403

A LEFT JOINабсолютно не швидше ніж INNER JOIN. Насправді це повільніше; за визначенням, зовнішнє з'єднання (LEFT JOIN або RIGHT JOIN) повинно виконувати всю роботу INNER JOINплюс додаткову роботу з розширенням нуля результатів. Очікується також повернути більше рядків, додатково збільшуючи загальний час виконання просто за рахунок збільшення розміру набору результатів.

(І навіть якщо a LEFT JOIN були швидшими в конкретних ситуаціях через якесь важко уявити злиття факторів, він функціонально не еквівалентний INNER JOIN, тому ви не можете просто замінити всі екземпляри одного іншим!)

Швидше за все, ваші проблеми з працездатністю полягають в іншому місці, наприклад, у тому, щоб не було правильно індексовано кандидатський ключ або зовнішній ключ. 9 таблиць досить багато для приєднання, тому уповільнення може буквально бути де завгодно. Якщо ви опублікуєте свою схему, ми можемо надати більше деталей.


Редагувати:

Розмірковуючи далі про це, я міг би подумати про одну обставину, за якої LEFT JOINможе бути швидше, ніж на INNER JOIN, і це коли:

  • Деякі таблиці дуже маленькі (скажімо, під 10 рядів);
  • У таблицях недостатньо індексів для покриття запиту.

Розглянемо цей приклад:

CREATE TABLE #Test1
(
    ID int NOT NULL PRIMARY KEY,
    Name varchar(50) NOT NULL
)
INSERT #Test1 (ID, Name) VALUES (1, 'One')
INSERT #Test1 (ID, Name) VALUES (2, 'Two')
INSERT #Test1 (ID, Name) VALUES (3, 'Three')
INSERT #Test1 (ID, Name) VALUES (4, 'Four')
INSERT #Test1 (ID, Name) VALUES (5, 'Five')

CREATE TABLE #Test2
(
    ID int NOT NULL PRIMARY KEY,
    Name varchar(50) NOT NULL
)
INSERT #Test2 (ID, Name) VALUES (1, 'One')
INSERT #Test2 (ID, Name) VALUES (2, 'Two')
INSERT #Test2 (ID, Name) VALUES (3, 'Three')
INSERT #Test2 (ID, Name) VALUES (4, 'Four')
INSERT #Test2 (ID, Name) VALUES (5, 'Five')

SELECT *
FROM #Test1 t1
INNER JOIN #Test2 t2
ON t2.Name = t1.Name

SELECT *
FROM #Test1 t1
LEFT JOIN #Test2 t2
ON t2.Name = t1.Name

DROP TABLE #Test1
DROP TABLE #Test2

Якщо запустити це і переглянути план виконання, ви побачите, що INNER JOINзапит дійсно коштує дорожче LEFT JOIN, тому що він відповідає двом вище критеріям. Це тому, що SQL Server хоче виконати хеш-відповідність для INNER JOIN, але вкладених циклів для LEFT JOIN; перший зазвичай набагато швидший, але оскільки кількість рядків настільки невелика і немає індексу для використання, операція хешування виявляється найдорожчою частиною запиту.

Ви можете побачити той же ефект, написавши програму улюбленою мовою програмування для виконання великої кількості пошукових запитів у списку з 5 елементами, порівняно з хеш-таблицею з 5 елементами. Через розмір версія хеш-таблиці насправді повільніше. Але збільште його до 50 елементів, або 5000 елементів, і версія списку сповільнюється до сканування, оскільки це хештелі O (N) проти O (1).

Але змініть цей запит, щоб він був у IDстовпці, а Nameви побачите зовсім іншу історію. У цьому випадку вона робить вкладені петлі для обох запитів, але INNER JOINверсія здатна замінити одне кластеризоване сканування індексу на пошук - це означає, що це буде буквально на порядок швидше з великою кількістю рядків.

Тож висновок є більш-менш тим, що я згадав кілька абзаців вище; це майже напевно проблема індексації чи покриття індексу, можливо, поєднана з однією або кількома дуже маленькими таблицями. Це єдині обставини, за яких SQL Server іноді може вибрати гірший план виконання для, INNER JOINніж a LEFT JOIN.


4
Існує ще один сценарій, який може призвести до того, що ВИХІДНЕ ПРИЄДНАННЯ буде більш ефективною, ніж ВНУТРІШНЯ ПРИЄДНАННЯ. Дивіться мою відповідь нижче.
dbenham

12
Хочу зазначити, що в основному немає документації на базу даних, яка б підтримувала ідею, що внутрішня і зовнішня приєднуються до продуктивності по-різному. Зовнішні з'єднання дещо дорожчі, ніж внутрішні з'єднання, через об'єм даних та розмір набору результатів. Однак основні алгоритми ( msdn.microsoft.com/en-us/library/ms191426(v=sql.105).aspx ) однакові для обох типів приєднань. Продуктивність повинна бути подібною, коли вони повертають однакові обсяги даних.
Гордон Лінофф

3
@Aaronaught. . . На цю відповідь посилався в коментарі, який сказав щось про те, що "зовнішні з'єднання виконуються значно гірше, ніж внутрішні з'єднання". Я прокоментував лише те, щоб переконатися, що це неправильне тлумачення не поширюється.
Гордон Лінофф

16
Я думаю, що ця відповідь вводить в оману в одному важливому аспекті: тому що в ньому йдеться про "ЛІВНЕ ПРИЄДНАННЯ абсолютно не швидше, ніж ВНУТРІШНЕ ПРИЄДНАННЯ". Цей рядок невірний. Це теоретично не швидше , ніж внутрішнє з'єднання. Це НЕ «абсолютно не швидше.» Питання - це конкретно питання про ефективність. На практиці я зараз бачив декілька систем (дуже великими компаніями!), Де INNER JOIN був смішно повільним порівняно з OUTER JOIN. Теорія та практика - це дуже різні речі.
Девід Френкель

5
@DavidFrenkel: Це малоймовірно. Я б попросив ознайомитись із порівнянням та перевіркою планів, якщо ви вважаєте, що таке розбіжність можливе. Можливо, це пов'язано з кешованими планами запитів / виконання або поганою статистикою.
Aaronaught

127

Є один важливий сценарій, який може призвести до того, що зовнішнє з'єднання буде швидшим, ніж внутрішнє з'єднання, про яке ще не говорилося.

При використанні зовнішнього з'єднання оптимізатор завжди може скинути зовнішню з'єднану таблицю з плану виконання, якщо стовпці з'єднання є ПК зовнішньої таблиці, і жоден із стовпців зовнішньої таблиці не посилається поза самим зовнішнім з'єднанням. Наприклад, SELECT A.* FROM A LEFT OUTER JOIN B ON A.KEY=B.KEYі B.KEY - це ПК для B. І Oracle (я вважаю, я використовував випуск 10), і Sql Server (я використав 2008 R2) обріжте таблицю B з плану виконання.

Це не обов'язково стосується внутрішнього з'єднання: SELECT A.* FROM A INNER JOIN B ON A.KEY=B.KEYможе або не вимагає B у плані виконання залежно від того, які обмеження існують.

Якщо A.KEY є нульовим зовнішнім ключем, що посилається на B.KEY, оптимізатор не може скинути B з плану, оскільки він повинен підтвердити, що B рядок існує для кожного рядка A.

Якщо A.KEY є обов'язковим зовнішнім ключем, що посилається на B.KEY, оптимізатор може скинути B з плану, оскільки обмеження гарантують існування рядка. Але те, що оптимізатор може скинути таблицю з плану, не означає, що це буде. SQL Server 2008 R2 НЕ скидає B із плану. Oracle 10 НЕ скидає B з плану. Неважко зрозуміти, як зовнішнє з'єднання буде виконувати внутрішнє з'єднання на SQL Server у цьому випадку.

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

Але це може бути дуже важливим дизайном при розгляді проекту. Часто створюється перегляд "робити все", який поєднує все, що може знадобитися користувачеві, пов'язане з центральною таблицею. (Особливо, якщо є наївні користувачі, які виконують спеціальні запити, які не розуміють реляційну модель). Перегляд може містити всі необхідні стовпці з багатьох таблиць. Але кінцеві користувачі можуть отримувати доступ до стовпців лише з підмножини таблиць у представленні. Якщо таблиці з'єднані із зовнішніми об'єднаннями, оптимізатор може (і робить) викинути непотрібні таблиці з плану.

Вкрай важливо переконатися, що вид з використанням зовнішніх з'єднань дає правильні результати. Як сказав Aaronaught - ви не можете сліпо підміняти ВНУТРІШНЕ ПРИЄДНАННЯ на ВНУТРІШНЕ ПРИЄДНАННЯ і очікувати тих же результатів. Але бувають випадки, коли це може бути корисно з міркувань продуктивності при використанні представлень.

Останнє зауваження - я не перевіряв вплив на продуктивність у світлі вищевикладеного, але теоретично, здається, ви повинні бути в змозі безпечно замінити ВНУТРІШНЕ ПРИЄДНАННЯ НА ЗОВНІШНІ ПРИЄДНАННЯ, якщо ви також додасте умову <FOREIGN_KEY> НЕ НУЛЬНО до пункту де


5
Я фактично зіткнувся з цією проблемою, коли розробляв надзвичайно динамічні запити. Я залишив у ВНУТРІШНІЙ ПРИЄДНАННІ, що я використовував і не витягував дані, і коли я переключив її на ЛІВНЕ ПРИЄДНАННЯ (з цікавості зсуву), запит насправді запустився швидше.
Ерік Філіпс

1
EDIT - Уточнено умови, які повинні існувати, щоб оптимізатор викинув зовнішню з'єднану таблицю з плану виконання.
dbenham

2
Одне незначне уточнення до вашої відповіді: Коли стовпець із зовнішнім ключем є ненульовим, INNER JOIN та LEFT JOIN стають семантично еквівалентними (тобто ваш запропонований пункт WHERE є зайвим); єдиною різницею був би план виконання.
Дуглас

2
Хоча це насправді показує тривіальний приклад, це надзвичайно глибока відповідь!
пбалага

6
+1: Я, здається, зіткнувся з цим у кількох запитах, де я використовував внутрішнє з'єднання з дуже великими таблицями. Внутрішнє з'єднання спричиняло розлив у tempdb в плані запитів (я припускаю, що з причини, зазначеної вище - і моєму серверу не вистачало оперативної пам'яті, щоб утримати все в пам'яті). Перехід на приєднання вліво ліквідував розлив на tempdb, в результаті чого деякі мої запити на 20-30 секунд зараз працюють у частках секунди. Це дуже важлива ситуація, коли більшість людей, мабуть, роблять припущення, що внутрішні з'єднання проходять швидше.
phosplait

23

Якщо все працює так, як слід, але ми всі знаємо, що все не працює так, як слід, особливо якщо мова йде про оптимізатор запитів, кешування плану запитів та статистику.

Спочатку я б запропонував відновити індекс та статистику, а потім очистити кеш-план плану запитів, щоб переконатися, що це не накручує справи. Однак у мене виникли проблеми, навіть коли це було зроблено.

Я переживав деякі випадки, коли ліве з'єднання було швидше, ніж внутрішнє з'єднання.

Основна причина така: Якщо у вас дві таблиці, і ви приєднуєтесь до стовпця з індексом (в обох таблицях). Внутрішнє з'єднання дасть той самий результат, незалежно від того, якщо ви переведіть на записи в індексі таблиці 1 та збігаєтеся з індексом на таблиці 2 так, як якщо б ви зробили зворотний: переведіть на записи в індексі таблиці 2 та збігайтесь з індексом в таблиці перша. Проблема полягає в тому, що у вас є оманлива статистика, оптимізатор запитів використовуватиме статистику індексу для пошуку таблиці з найменшими відповідніми записами (виходячи з інших ваших критеріїв). Якщо у вас дві таблиці, по 1 мільйон у кожній, у першій таблиці 10 збігів рядків, а у другій таблиці - 100000 рядків. Найкращим способом було б провести сканування індексу на першій таблиці та зіставити 10 разів у таблиці дві. Зворотним було б індексне сканування, яке містить цикл понад 100000 рядків і намагається відповідати 100000 разів, і лише 10 досягають успіху. Тож якщо статистика не є правильною, оптимізатор може вибрати неправильну таблицю та індекс, щоб перейти на цикл.

Якщо оптимізатор вирішить оптимізувати ліве з'єднання в порядку запису, він буде працювати краще, ніж внутрішнє з'єднання.

АЛЕ, оптимізатор може також оптимізувати ліве з'єднання під оптимальним варіантом як ліве напівприєднання. Щоб вибрати, який ви хочете, ви можете скористатись підказом про примусовий наказ.


18

Спробуйте обидва запити (той, що має внутрішній і лівий приєднання), OPTION (FORCE ORDER)в кінці та опублікуйте результати. OPTION (FORCE ORDER)- це підказка запиту, яка змушує оптимізатора будувати план виконання з порядком приєднання, який ви вказали в запиті.

Якщо INNER JOINпочне виконувати так швидко LEFT JOIN, це тому, що:

  • У запиті, повністю складеному користувачем INNER JOIN s, порядок з'єднання не має значення. Це дає свободу оптимізатору запитів замовляти приєднання так, як він вважає за потрібне, тому проблема може покладатися на оптимізатор.
  • З LEFT JOIN, це не так, оскільки зміна порядку з'єднання змінить результати запиту. Це означає, що двигун повинен виконувати порядок з'єднання, який ви вказали у запиті, що може бути краще, ніж оптимізоване.

Не знаю, чи відповідає це на ваше запитання, але я колись був у проекті, в якому були дуже складні запити, які робили розрахунки, які повністю заплутали оптимізатор. У нас були випадки, коли a FORCE ORDERзменшив час виконання запиту з 5 хвилин до 10 секунд.


9

Зробив ряд порівнянь між лівим зовнішнім і внутрішнім з'єднанням і не зміг знайти стійку різницю. Існує багато змінних. Я працюю над базою звітів з тисячами таблиць, багато з великою кількістю полів, багато змін з часом (версії постачальників та локальний робочий процес). Неможливо створити всі комбінації покриття індексів для задоволення потреб такого широкого спектру запитів та обробки історичних даних. Побачили, що внутрішні запити вбивають продуктивність сервера, тому що дві великі (мільйони до десятків мільйонів рядків) таблиці є внутрішніми об'єднаними обома, що тягнуть велику кількість полів, а індекс покриття не існує.

Однак, найбільше питання, схоже, не виникає в обговоренні вище. Можливо, ваша база даних добре розроблена з тригерами та добре продуманою обробкою транзакцій для забезпечення хороших даних. У шахти часто є значення NULL там, де їх не очікується. Так, визначення таблиць можуть примусово застосовувати no-Nulls, але це не є варіантом у моєму середовищі.

Тож питання полягає в тому, чи ви розробляєте запит лише на швидкість, що є більш високим пріоритетом для обробки транзакцій, яка виконує один і той же код тисячі разів на хвилину. Або ви шукаєте точність, яку забезпечить лівий зовнішній з’єднання. Пам'ятайте, що внутрішні з'єднання повинні знаходити збіги з обох сторін, тому несподіваний NULL не буде видаляти дані з двох таблиць, але, можливо, цілі рядки інформації. І це буває так приємно, жодних повідомлень про помилки.

Ви можете бути дуже швидкими, оскільки отримуєте 90% необхідних даних і не виявляєте внутрішні приєднання, мовчки видаливши інформацію. Іноді внутрішні приєднання можуть бути швидшими, але я не вірю, що хтось зробив це припущення, якщо не переглянув план виконання. Швидкість важлива, але важливіша точність.


8

Проблеми з вашою ефективністю, швидше за все, пов’язані з кількістю приєднань, які ви робите, і чи мають стовпці, до яких ви приєднуєтесь, індекси чи ні.

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


7

Зовнішнє з'єднання може забезпечити чудову ефективність при використанні в представленнях.

Скажімо, у вас є запит, що включає перегляд, і цей перегляд складається з 10 з’єднаних разом таблиць. Скажімо, ваш запит трапляється лише для використання стовпців із 3 із цих 10 таблиць.

Якби ці 10 таблиць були об'єднані між собою, тоді оптимізатору запитів доведеться приєднатись до них усіх, хоча самому вашому запиту не потрібно 7 із 10 таблиць. Це тому, що внутрішні з'єднання самі можуть фільтрувати дані, роблячи їх важливими для обчислення.

Якщо ці 10 таблиць були з'єднані зовнішньо замість них об'єднані , оптимізатор запитів б лише до тих, які були необхідні: 3 з 10 у цьому випадку. Це тому, що самі об'єднання вже не фільтрують дані, і таким чином невикористані приєднання можна пропустити.

Джерело: http://www.sqlservercentral.com/blogs/sql_coach/2010/07/29/poor-little-misunderposed-views/


1
Ваше твердження про "приєднані до зовнішньої сторони" є оманливим та потенційно невірним. Зовнішні означає, що дані з іншого боку не повинні існувати - і якщо вони не замінюють NULL. За певних обставин RDBMS може "пропустити" їх (див. Вище відповідь від dbenham). ЯК ВЗАЄМО - зовнішній та внутрішній можуть призвести до того, що ваш запит поверне докорінно різні результати. INNER означає - дайте результати, для яких елемент знаходиться в BOTH A & B. LEFT OUTER означає все A, і необов'язково B, якщо воно існує. Перший випадок - ви отримуєте кілька рядків, у другому - ВСІ рядки.
ripvlan

1
@ripvlan Звичайно, зовнішні та внутрішні з'єднання не завжди взаємозамінні. Первісне питання стосувалося продуктивності, що означає, що ми говоримо про випадки, коли будь-який приєднання поверне той же набір результатів.
MarredCheese

1
Так і - ЗОВНІШНИЙ може спричинити проблеми з продуктивністю, оскільки це призведе до повернення всіх рядків (більше даних). Ваше припущення, що запити призводять до одного виходу, є справедливим, однак це не вірно в загальному випадку і характерне для кожного дизайну db. А для тих, хто не на 100% знайомий з реляційною алгеброю, це може викликати у них горе. Моя думка полягає лише в тому, щоб запропонувати більше розуміння людям, які читають це, шукаючи поради, і те, що ЛІВО / ПРАВО не магічно вирішить проблему і може викликати більше проблем. Це потужність, що залишилася на рівні 300 :-)
ripvlan

2

Я виявив щось цікаве на SQL сервері, коли перевіряв, чи внутрішні з'єднання швидші, ніж ліві з'єднання.

Якщо ви не включаєте елементи лівої об’єднаної таблиці, у операторі select ліве з'єднання буде швидше, ніж той самий запит із внутрішнім об'єднанням.

Якщо ви включите ліву об'єднану таблицю в оператор select, внутрішнє з'єднання з тим же запитом було рівним або швидшим, ніж ліве з'єднання.


0

Зі своїх порівнянь я виявляю, що вони мають точно такий же план виконання. Існує три сценарії:

  1. Якщо і коли вони повертають однакові результати, вони мають однакову швидкість. Однак ми маємо пам’ятати, що це не ті самі запити, і що LEFT JOIN, можливо, поверне більше результатів (коли деякі умови ON не будуть дотримані) --- тому це зазвичай повільніше.

  2. Коли основна таблиця (перша, що не стосується const в плані виконання) має обмежувальну умову (WHERE id =?) І відповідна умова ON на значення NULL, "правильна" таблиця не приєднується --- це коли ЛІВО ПРИЄДНАЙТЕСЬ швидше.

  3. Як обговорюється в пункті 1, зазвичай ВНУТРІШНЕ ПРИЄДНАННЯ є більш обмежувальним і дає менше результатів і, отже, швидше.

Обидва використовують (однакові) індекси.

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