Який запит SQL швидший? Фільтрувати за критеріями Приєднання або Де?


98

Порівняйте ці 2 запити. Чи швидше поставити фільтр за критеріями об'єднання або в WHEREпункті? Я завжди відчував, що це швидше за критеріями приєднання, оскільки це скорочує набір результатів якомога швидше, але я точно не знаю.

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

Запит 1

SELECT      *
FROM        TableA a
INNER JOIN  TableXRef x
        ON  a.ID = x.TableAID
INNER JOIN  TableB b
        ON  x.TableBID = b.ID
WHERE       a.ID = 1            /* <-- Filter here? */

Запит 2

SELECT      *
FROM        TableA a
INNER JOIN  TableXRef x
        ON  a.ID = x.TableAID
        AND a.ID = 1            /* <-- Or filter here? */
INNER JOIN  TableB b
        ON  x.TableBID = b.ID

РЕДАГУВАТИ

Я провів кілька тестів, і результати показують, що насправді це дуже близько, але це WHEREположення насправді трохи швидше! =)

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

МИНУЛИЙ ЧАС, ДЕ КРИТЕРІЇ: 143016 мс МИНУТИЙ
ЧАС, КРИТЕРІЇ: 143256 мс

ТЕСТ

SET NOCOUNT ON;

DECLARE @num    INT,
        @iter   INT

SELECT  @num    = 1000, -- Number of records in TableA and TableB, the cross table is populated with a CROSS JOIN from A to B
        @iter   = 1000  -- Number of select iterations to perform

DECLARE @a TABLE (
        id INT
)

DECLARE @b TABLE (
        id INT
)

DECLARE @x TABLE (
        aid INT,
        bid INT
)

DECLARE @num_curr INT
SELECT  @num_curr = 1
        
WHILE (@num_curr <= @num)
BEGIN
    INSERT @a (id) SELECT @num_curr
    INSERT @b (id) SELECT @num_curr
    
    SELECT @num_curr = @num_curr + 1
END

INSERT      @x (aid, bid)
SELECT      a.id,
            b.id
FROM        @a a
CROSS JOIN  @b b

/*
    TEST
*/
DECLARE @begin_where    DATETIME,
        @end_where      DATETIME,
        @count_where    INT,
        @begin_join     DATETIME,
        @end_join       DATETIME,
        @count_join     INT,
        @curr           INT,
        @aid            INT

DECLARE @temp TABLE (
        curr    INT,
        aid     INT,
        bid     INT
)

DELETE FROM @temp

SELECT  @curr   = 0,
        @aid    = 50

SELECT  @begin_where = CURRENT_TIMESTAMP
WHILE (@curr < @iter)
BEGIN
    INSERT      @temp (curr, aid, bid)
    SELECT      @curr,
                aid,
                bid
    FROM        @a a
    INNER JOIN  @x x
            ON  a.id = x.aid
    INNER JOIN  @b b
            ON  x.bid = b.id
    WHERE       a.id = @aid
        
    SELECT @curr = @curr + 1
END
SELECT  @end_where = CURRENT_TIMESTAMP

SELECT  @count_where = COUNT(1) FROM @temp
DELETE FROM @temp

SELECT  @curr = 0
SELECT  @begin_join = CURRENT_TIMESTAMP
WHILE (@curr < @iter)
BEGIN
    INSERT      @temp (curr, aid, bid)
    SELECT      @curr,
                aid,
                bid
    FROM        @a a
    INNER JOIN  @x x
            ON  a.id = x.aid
            AND a.id = @aid
    INNER JOIN  @b b
            ON  x.bid = b.id
    
    SELECT @curr = @curr + 1
END
SELECT  @end_join = CURRENT_TIMESTAMP

SELECT  @count_join = COUNT(1) FROM @temp
DELETE FROM @temp

SELECT  @count_where AS count_where,
        @count_join AS count_join,
        DATEDIFF(millisecond, @begin_where, @end_where) AS elapsed_where,
        DATEDIFF(millisecond, @begin_join, @end_join) AS elapsed_join

10
Залежно від даних, критерії WHERE проти JOIN можуть повертати різні набори результатів.
OMG Ponies

4
@OMG Ponies дуже вірно, але багато разів це не так добре.
Джон Еріксон,

2
Я б не називав різницю нижче 5% як різницю - вони однакові. Вам потрібна значимість для різниці в 2 %%, краще проведіть тести 1000 разів, щоб переконатися, що це не просто випадкові випадки.
TomTom

Перевага полягає у фільтруванні даних перед приєднанням, тому, якби це був x.ID, ви б скоріше побачили покращення, ніж з a.ID
MikeT

Відповіді:


65

З точки зору продуктивності вони однакові (і складають однакові плани)

Логічно, ви повинні зробити операцію, яка все ще має сенс, якщо замінити INNER JOINна LEFT JOIN.

У вашому випадку це буде виглядати так:

SELECT  *
FROM    TableA a
LEFT JOIN
        TableXRef x
ON      x.TableAID = a.ID
        AND a.ID = 1
LEFT JOIN
        TableB b
ON      x.TableBID = b.ID

або це:

SELECT  *
FROM    TableA a
LEFT JOIN
        TableXRef x
ON      x.TableAID = a.ID
LEFT JOIN
        TableB b
ON      b.id = x.TableBID
WHERE   a.id = 1

Перший запит не поверне жодних фактичних збігів для a.idінших, ніж 1, тому другий синтаксис (з WHERE) логічно є більш послідовним.


Коли я малював множини, я зрозумів, чому другий випадок є більш послідовним. У попередньому запиті обмеження a.id = 1застосовується лише до перетину, а не до лівої частини, за винятком перетину.
FtheBuilder

1
У першому прикладі можуть бути рядки where a.id != 1, в іншому будуть лише рядки where a.id = 1.
FtheBuilder

1
Ваша мова незрозуміла. "Логічно, вам слід зробити операцію, яка все ще має сенс, якщо ..." і "логічно більш послідовною", немає сенсу. Чи можете ви перефразувати?
Філіпсія

24

Для внутрішніх приєднань не має значення, куди ви ставите свої критерії. Компілятор SQL перетворить і те, і інше на план виконання, в якому фільтрація відбувається під об'єднанням (тобто, як якщо б вираз фільтра з'явився, це в умові об'єднання).

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


Отже, у внутрішньому з'єднанні він спочатку обчислює фільтр, а потім приєднує висновок фільтра з іншою таблицею або спочатку приєднує дві таблиці, а потім застосовує фільтр?
Ashwin

@Remus Rusanu - не могли б Ви детальніше розповісти про те, як змінюється семантика у разі Зовнішнього приєднання? Я отримую різні результати залежно від положення фільтра, але не можу зрозуміти, чому
Анант

3
@Ananth із зовнішнім об’єднанням ви отримуєте NULL для всіх стовпців об’єднаної таблиці, де умова JOIN не відповідає. Фільтри не задовольняють значення NULL і ліквідують рядки, перетворюючи ВНІШНЄ приєднання фактично у ВНУТРІШНЄ.
Ремус Русану,

@Ananth Я досяг необхідних оптимізацій на основі Вашого коментаря. Моя зміна полягала з WHERE x.TableAID = a.ID або x.TableAID є нульовим значенням ON x.TableAID = a.ID. Змінюючи розташування фільтра на ЗОВНІШНІЙ приєднанні, повідомте компілятору фільтрувати, а потім приєднуватись, а не приєднуватись, а потім фільтрувати. Він також міг використовувати індекс у цьому стовпці, оскільки він не повинен відповідати Null. Відповідь запиту змінено з 61 секунди на 2 секунди.
Бен Грипка

10

Що стосується двох методів.

  • JOIN / ON - для об’єднання таблиць
  • WHERE - для фільтрації результатів

Хоча ви можете використовувати їх по-різному, мені це завжди здається запахом.

Займіться продуктивністю, коли це проблема. Тоді ви можете розглянути такі "оптимізації".


2

З будь-яким оптимізатором запитів, який коштує цент .... вони ідентичні.


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

2
Перевірте це за реального навантаження. В основному - якщо вони генерують однаковий план виконання, вони ... ідентичні за роботою. Принаймні для звичайних / простих випадків (тобто не для того, щоб приєднати 14 таблиць) я цілком впевнений, що вони ідентичні;)
TomTom

1

У postgresql вони однакові. Ми знаємо це, тому що якщо ви виконуєте explain analyzeкожен із запитів, план виявляється однаковим. Візьмемо цей приклад:

# explain analyze select e.* from event e join result r on e.id = r.event_id and r.team_2_score=24;

                                                  QUERY PLAN                                                   
---------------------------------------------------------------------------------------------------------------
 Hash Join  (cost=27.09..38.22 rows=7 width=899) (actual time=0.045..0.047 rows=1 loops=1)
   Hash Cond: (e.id = r.event_id)
   ->  Seq Scan on event e  (cost=0.00..10.80 rows=80 width=899) (actual time=0.009..0.010 rows=2 loops=1)
   ->  Hash  (cost=27.00..27.00 rows=7 width=8) (actual time=0.017..0.017 rows=1 loops=1)
         Buckets: 1024  Batches: 1  Memory Usage: 9kB
         ->  Seq Scan on result r  (cost=0.00..27.00 rows=7 width=8) (actual time=0.006..0.008 rows=1 loops=1)
               Filter: (team_2_score = 24)
               Rows Removed by Filter: 1
 Planning time: 0.182 ms
 Execution time: 0.101 ms
(10 rows)

# explain analyze select e.* from event e join result r on e.id = r.event_id where r.team_2_score=24;
                                                  QUERY PLAN                                                   
---------------------------------------------------------------------------------------------------------------
 Hash Join  (cost=27.09..38.22 rows=7 width=899) (actual time=0.027..0.029 rows=1 loops=1)
   Hash Cond: (e.id = r.event_id)
   ->  Seq Scan on event e  (cost=0.00..10.80 rows=80 width=899) (actual time=0.010..0.011 rows=2 loops=1)
   ->  Hash  (cost=27.00..27.00 rows=7 width=8) (actual time=0.010..0.010 rows=1 loops=1)
         Buckets: 1024  Batches: 1  Memory Usage: 9kB
         ->  Seq Scan on result r  (cost=0.00..27.00 rows=7 width=8) (actual time=0.006..0.007 rows=1 loops=1)
               Filter: (team_2_score = 24)
               Rows Removed by Filter: 1
 Planning time: 0.140 ms
 Execution time: 0.058 ms
(10 rows)

Вони обидва мають однакову мінімальну та максимальну вартість, а також однаковий план запитів. Також зверніть увагу, що навіть у верхньому запиті team_score_2 застосовується як "Фільтр".


0

Навряд чи розміщення цього об’єднання буде вирішальним фактором для продуктивності. Я не знайомий з плануванням виконання tsql, але, швидше за все, вони будуть автоматично оптимізовані до подібних планів.


0

Правило № 0: Запустіть деякі тести і подивіться! Єдиний спосіб по-справжньому визначити, що буде швидше - це спробувати. Такі типи тестів дуже легко виконати за допомогою SQL-профілі.

Також вивчіть план виконання запиту, написаного за допомогою JOIN та речення WHERE, щоб побачити, які відмінності виділяються.

Нарешті, як сказали інші, будь-який гідний оптимізатор, включаючи вбудований у SQL Server, повинен поводитися з цими двома ідентично.


Але тільки для внутрішніх приєднань. Набір результатів буде дуже різним для вихідних приєднань.
HLGEM

Звичайно. На щастя, у наведеному прикладі використовуються внутрішні об’єднання.
3Dave

1
На жаль, питання стосується об'єднань, а не внутрішніх об'єднань.
Павло,

Так, Девід, питання стосується приєднання. Зразок, що підтримує питання, використовує внутрішні об'єднання.
Пол

0

Це швидше? Спробуйте і подивіться.

Що легше читати? Перший для мене виглядає більш "правильним", оскільки переміщений стан насправді не має нічого спільного з приєднанням.


0

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

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