Optymalizator MySQL – ufaj, ale sprawdzaj

Wedle definicji, SQL jest językiem deklaratywnym. Oznacza to, że programista opisuje, co chce osiągnąć, natomiast sposób osiągnięcia celu pozostawia silnikowi bazy danych. Ta zacna reguła zazwyczaj się sprawdza, dzięki czemu pamiętając o zaledwie kilku regułach można tworzyć zgrabne i szybkie zapytania. Czasami jednak optymalizator podstawia nogę w zupełnie nieoczekiwanym momencie.

Ostatnio miałem następującą sytuację: w pewnej firmie, zajmującej się przekazywaniem dotacji dobroczynnych, trzeba utworzyć raport – listę organizacji, na rzecz których w zadanym okresie czasu dokonano co najmniej jednej donacji.

Zadanie nie wygląda na trudne. Do wykonania raportu wystarczy przeanalizować jedną tabelę myc_charity (listę organizacji – 750 rekordów) i jeden prosty widok  v_eventdonations (dane wykonanych donacji, ok 500 tysięcy rekordów). Widok powstał z normalnego złączenia 4 tabel, nie zawiera żadnych złożonych warunków, funkcji agregujących czy innych takich wynalazków. Defacto, jest tylko „skrótem”, stosowanym dla uniknięcia powtarzających się złączeń w kolejnych zapytaniach.

Pierwsze podejście do zadania wyglądało tak:

select
 CharityID, Name AS CharityName, OrgID
from
 myc_charity
where
CharityID in (
 select distinct ved.CharityID from v_eventdonations ved
 where
   ved.PaymentDate >= '2012-10-1'
   and ved.PaymentDate <= '2012-10-31'
   and (DEStatusID & 1) > 0
);

Rezultat: 235 rekordów. Czas wykonania: 619 sekund. Absurdalny! Tym bardziej absurdalny, że myc_charity.CharityID jest kluczem głównym, a samo podzapytanie wykonuje się w 0.81 sekundy.

Zwykle podejrzenie pada na skorelowanie podzapytania, ale w tym wypadku nie byłem w stanie znaleźć korelacji. Co prawda czas wykonania wskazywał na jej istnienie: 750 * 0.81 = 607 sekund, czyli niemal dokładnie tyle, ile zajmowało wygenerowanie raportu. Polecenie EXPLAIN również twierdziło, że zapytanie jest skorelowane:

mysql> explain select CharityID, Name AS CharityName, OrgID from myc_charity whe
re CharityID in (select distinct ved.CharityID from v_eventdonations ved where v
ed.PaymentDate >= '2012-10-1'
-> and ved.PaymentDate <= '2012-10-31'
-> and (DEStatusID & 1) > 0
-> )\G
*************************** 1. row ***************************
id: 1
select_type: PRIMARY
table: myc_charity
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 1228
Extra: Using where
*************************** 2. row ***************************
id: 2
select_type: DEPENDENT SUBQUERY
table: utc
type: ref
possible_keys: PRIMARY,UserEventID_2,UserEventID,CharityID
key: CharityID
key_len: 4
ref: func
rows: 27
Extra: Using where; Using temporary
[.........]

Dopiero po wykonaniu EXPLAIN wykonanie SHOW WARNINGS pokazuje perfidię optymalizatora:

mysql> show warnings\G
*************************** 1. row ***************************
  Level: Note
   Code: 1003
Message: select
  `mycharity_live`.`myc_charity`.`CharityID` AS `CharityID`,
  `mycharity_live`.`myc_charity`.`Name` AS `CharityName`,
  `mycharity_live`.`myc_charity`.`OrgID` AS `OrgID`
from
  `mycharity_live`.`myc_charity`
where <in_optimizer>(
  `mycharity_live`.`myc_charity`.`CharityID`,
  <exists>(
    select distinct 1
    from `mycharity_live`.`myc_userevents` `ev`
      join `mycharity_live`.`myc_userevent_to_charity` `utc`
      join `mycharity_live`.`myc_donationevent_to_charity` `dtc`
      join `mycharity_live`.`myc_donationevent` `de`
    where (
      (`mycharity_live`.`ev`.`UserEventID` = `mycharity_live`.`utc`.`UserEventID`)
      and (`mycharity_live`.`dtc`.`UserEventCharityID` = `mycharity_live`.`utc`.`UserEventCharityID`)
      and (`mycharity_live`.`de`.`DonationEventID` = `mycharity_live`.`dtc`.`DonationEventID`)
      and (`mycharity_live`.`de`.`PaymentDate` >= '2012-10-1')
      and (`mycharity_live`.`de`.`PaymentDate` <= '2012-10-31')
      and (
        (`mycharity_live`.`de`.`DEStatusID` & 1) > 0)
        and (<cache>(`mycharity_live`.`myc_charity`.`CharityID`) = `mycharity_live`.`utc`.`CharityID`
      )
)))
1 row in set (0.00 sec)

Cóż się tu wydarzyło? Ano, optymalizator „zoptymalizował” zapytanie. W tym niestety przypadku zamienił operator IN na operator EXISTS, a zapytanie wewnętrzne skorelował (ostatni warunek) z zapytaniem zewnętrznym. Efektem była fatalna wydajność tego, wydawałoby się, prostego zapytania.

Jak wybrnąć z tego problemu? Trzeba się odwołać do jednej z podstawowych wskazówek SQL-a: w miarę możliwości przesuwaj warunki z WHERE do złączeń.

select
    distinct c.CharityID, c.`Name` AS CharityName, c.OrgID
from
    myc_charity c
    left join v_eventdonations ved on ved.CharityID = c.CharityID
        and ved.PaymentDate >= '2012-10-1'
        and ved.PaymentDate <= '2012-10-31'
        and (ved.DEStatusID & 1) > 0
where ved.CharityID is not null;

Rezultaty: identyczne z pierwszym zapytaniem. Czas wykonania: 0.25 sekundy. Przyrost wydajności: 2500 razy :)

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

Możesz użyć następujących tagów oraz atrybutów HTML-a: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>