Какой SQL-запрос быстрее? Отфильтровать критерии присоединения или предложение "Где"?


99

Сравните эти 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 vs JOIN могут возвращать разные наборы результатов.
OMG Ponies,

4
@OMG Ponies - это правда, но во многих случаях это не так.
Джон Эриксон,

2
Разницу ниже 5% я бы не назвал разницей - они одинаковы. Вам нужна значимость разницы в 2 %%, лучше запустите тесты 1000 раз, чтобы убедиться, что это не просто случайность.
TomTom

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

Ответы:


66

С точки зрения производительности они одинаковы (и имеют одинаковые планы)

По логике, вы должны сделать операцию, которая все еще имеет смысл, если вы замените 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 - не могли бы вы подробнее рассказать о том, как изменяется семантика в случае внешнего соединения? Я получаю разные результаты в зависимости от положения фильтра, но не
Ananth

3
@Ananth с внешним соединением вы получаете NULL для всех столбцов объединенной таблицы, где условие JOIN не соответствует. Фильтры не удовлетворяют NULL и исключают строки, превращая соединение OUTER во внутреннее соединение.
Ремус Русану

@Ananth Я добился требуемой оптимизации на основе вашего комментария. Мое изменение было с WHERE x.TableAID = a.ID или x.TableAID is null на ON x.TableAID = a.ID. При изменении местоположения фильтра во ВНЕШНЕМ соединении компилятор должен знать, что нужно фильтровать, а затем объединить, а не объединить, а затем фильтровать. Он также мог использовать индекс в этом столбце, потому что он не должен был соответствовать Null. Ответ на запрос изменен с 61 секунды на 2 секунды.
Бен Грипка,

10

Что касается двух методов.

  • JOIN / ON - для объединения таблиц
  • ГДЕ для фильтрации результатов

Хотя вы можете использовать их по-разному, мне всегда кажется, что это запах.

Работайте с производительностью, когда это проблема. Тогда вы можете изучить такие «оптимизации».


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

Конечно. К счастью, в приведенном примере используются внутренние соединения.
3ave

1
К сожалению, речь идет о соединениях, а не о внутренних соединениях.
Пол

Да, Дэвид, речь идет о присоединениях. В образце, подтверждающем вопрос, используются внутренние соединения.
Пол

0

Это быстрее? Попробуйте и убедитесь.

Что легче читать? Первый вариант мне кажется более «правильным», поскольку перемещенное условие не имеет ничего общего с объединением.


0

Я предполагаю, что первый, потому что он делает более конкретный фильтр данных. Но вы должны увидеть план выполнения , как и при любой оптимизации, потому что он может сильно отличаться в зависимости от размера данных, серверного оборудования и т. Д.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.