Które zapytanie SQL jest szybsze? Filtruj według kryteriów dołączania lub klauzuli Where?

99

Porównaj te 2 zapytania. Czy szybciej jest umieścić filtr na kryteriach łączenia, czy w WHEREklauzuli. Zawsze czułem, że jest szybszy na kryteriach łączenia, ponieważ zmniejsza zestaw wyników w możliwie najszybszym momencie, ale nie wiem na pewno.

Zamierzam zbudować kilka testów do obejrzenia, ale chciałem też uzyskać opinie, które byłyby bardziej czytelne.

Zapytanie 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? */

Zapytanie 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

EDYTOWAĆ

Przeprowadziłem kilka testów i wyniki pokazują, że faktycznie jest bardzo blisko, ale WHEREklauzula jest faktycznie nieco szybsza! =)

Absolutnie zgadzam się, że bardziej sensowne jest zastosowanie filtru w WHEREklauzuli, byłem po prostu ciekawy konsekwencji wydajności.

CZAS OPRACOWANY, GDZIE KRYTERIA: 143016 ms
CZAS OPRACOWANY DOŁĄCZ KRYTERIA: 143256 ms

TEST

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
Jon Erickson
źródło
10
W zależności od danych kryteria WHERE vs JOIN mogą zwracać różne zestawy wyników.
Kucyki OMG
4
@OMG Kucyki to prawda, ale często nie tak dobrze.
Jon Erickson
2
Nie nazwałbym różnicy poniżej 5% różnicą - są takie same. Chcesz, aby różnica 2 %% była istotna, lepiej uruchom testy 1000 razy, aby upewnić się, że nie jest to tylko przypadek.
TomTom
Zaletą jest filtrowanie danych przed dołączeniem, więc jeśli byłby to x.ID, wtedy byłoby bardziej prawdopodobne, że zobaczysz poprawę niż w przypadku a.ID
MikeT

Odpowiedzi:

66

Pod względem wydajności są takie same (i produkują te same plany)

Logicznie rzecz biorąc, trzeba zrobić operację, która nadal ma sens, jeśli zastąpi INNER JOINz LEFT JOIN.

W twoim przypadku będzie to wyglądać tak:

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

albo to:

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

Pierwsze zapytanie nie zwróci żadnych rzeczywistych dopasowań dla a.idinnego niż 1, więc druga składnia (z WHERE) jest logicznie bardziej spójna.

Quassnoi
źródło
Kiedy rysowałem zestawy, zrozumiałem, dlaczego drugi przypadek jest bardziej spójny. W pierwszym zapytaniu ograniczenie a.id = 1dotyczy tylko skrzyżowania, a nie lewej części wyłączającej skrzyżowanie.
FtheBuilder
1
W pierwszym przykładzie mogą być wiersze, w których a.id != 1, w drugim będą tylko wiersze, w których a.id = 1.
FtheBuilder,
1
Twój język jest niejasny. „Logicznie rzecz biorąc, operacja, która nadal ma sens, jeśli…” i „logicznie bardziej spójna” nie mają sensu. Czy możesz to przeformułować?
philipxy
24

W przypadku złączeń wewnętrznych nie ma znaczenia, gdzie umieścisz swoje kryteria. Kompilator SQL przekształci oba w plan wykonania, w którym filtrowanie odbywa się poniżej złączenia (tj. Tak, jakby wyrażenia filtru znajdowały się w warunku złączenia).

Sprzężenia zewnętrzne to inna sprawa, ponieważ miejsce filtru zmienia semantykę zapytania.

Remus Rusanu
źródło
Zatem w przypadku łączenia wewnętrznego najpierw oblicza filtr, a następnie łączy dane wyjściowe filtru z drugą tabelą, czy też najpierw łączy dwie tabele, a następnie stosuje filtr?
Ashwin,
@Remus Rusanu - czy mógłbyś wyjaśnić, jak zmienia się semantyka w przypadku połączenia zewnętrznego? Otrzymuję różne wyniki w zależności od pozycji filtra, ale nie mogę zrozumieć, dlaczego
Ananth
3
@Ananth z łączeniem zewnętrznym otrzymujesz wartości NULL dla wszystkich kolumn połączonej tabeli, w których warunek JOIN nie jest zgodny. Filtry nie spełnią wartości NULL i wyeliminują wiersze, zmieniając w efekcie połączenie OUTER w złączenie INNER.
Remus Rusanu
@Ananth Osiągnąłem wymagane optymalizacje na podstawie Twojego komentarza. Moja zmiana nastąpiła z WHERE x.TableAID = a.ID lub x.TableAID jest null na ON x.TableAID = a.ID. Zmiana lokalizacji filtru na złączeniu OUTER pozwala kompilatorowi wiedzieć, że filtruje, a następnie łączy, a nie łączy, a następnie filtruje. Był również w stanie użyć indeksu w tej kolumnie, ponieważ nie musiał pasować do Null. Odpowiedź na zapytanie zmieniona z 61 sekund na 2 sekundy.
Ben Gripka
10

Jeśli chodzi o dwie metody.

  • JOIN / ON służy do łączenia stołów
  • WHERE służy do filtrowania wyników

Chociaż możesz ich używać inaczej, zawsze wydaje mi się to zapachem.

Zajmij się wydajnością, gdy jest to problem. Następnie możesz przyjrzeć się takim „optymalizacjom”.

Robin Day
źródło
2

Z każdym optymalizatorem zapytań będzie wart cent ... są identyczne.

TomTom
źródło
Jestem pewien, że przy każdym rzeczywistym obciążeniu nie są one identyczne. Jeśli prawie nie masz danych, pytanie jest bezwartościowe.
eKek0
2
Sprawdź to przy prawdziwym obciążeniu pracą. Zasadniczo - jeśli generują ten sam plan wykonania, to ... mają identyczną wydajność. Przynajmniej dla zwykłych / prostych przypadków (tj. Nie tego, który łączy 14 stolików) jestem całkiem pewien, że są identyczne;)
TomTom
1

W postgresql są takie same. Wiemy o tym, ponieważ jeśli zrobisz explain analyzena każdym z zapytań, plan będzie taki sam. Weź ten przykład:

# 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)

Oba mają ten sam minimalny i maksymalny koszt, a także ten sam plan zapytań. Zwróć także uwagę, że nawet w najwyższym zapytaniu team_score_2 jest stosowany jako „Filtr”.

Peter Graham
źródło
0

Jest naprawdę mało prawdopodobne, aby umieszczenie tego połączenia miało decydujący wpływ na wydajność. Nie jestem dokładnie zaznajomiony z planowaniem wykonania dla tsql, ale prawdopodobnie zostaną one automatycznie zoptymalizowane do podobnych planów.

Joseph Mastey
źródło
0

Zasada # 0: Przeprowadź testy porównawcze i zobacz! Jedynym sposobem, aby naprawdę stwierdzić, co będzie szybsze, jest wypróbowanie tego. Tego typu testy porównawcze są bardzo łatwe do wykonania przy użyciu profilera SQL.

Przeanalizuj również plan wykonania zapytania napisanego za pomocą klauzuli JOIN i WHERE, aby zobaczyć, jakie różnice się wyróżniają.

Wreszcie, jak powiedzieli inni, te dwa powinny być traktowane identycznie przez każdy przyzwoity optymalizator, w tym ten wbudowany w SQL Server.

3Dave
źródło
Ale tylko dla połączeń wewnętrznych. Zestaw wyników będzie bardzo różny dla złączeń wyjściowych.
HLGEM
Oczywiście. Na szczęście w podanym przykładzie zastosowano sprzężenia wewnętrzne.
Zapisz
1
Niestety, pytanie dotyczy złączeń, a nie złączeń wewnętrznych.
Paul
Tak David, pytanie dotyczy złączeń. Próbka wspierająca pytanie używa złączeń wewnętrznych.
Paul
0

Czy to jest szybsze? Spróbuj i zobacz.

Który jest łatwiejszy do odczytania? Pierwszy wydaje mi się bardziej „poprawny”, ponieważ przeniesiony warunek nie ma nic wspólnego ze złączeniem.

David M.
źródło
0

Myślę, że to pierwsze, ponieważ tworzy bardziej szczegółowy filtr danych. Ale powinieneś zobaczyć plan wykonania , jak w przypadku każdej optymalizacji, ponieważ może się on bardzo różnić w zależności od rozmiaru danych, sprzętu serwera itp.

eKek0
źródło