MySQL udostępnia bezpośredni dostęp do silników InnoDB i MyISAM – realizowany jest on przy pomocy polecenia HANDLER. Jego zasada działania sprowadza się z grubsza do tego, że operuje on na indeksie i pobiera rekord dla danego wpisu w indeksie. Wygląda to mniej więcej tak:

HANDLER tabela OPEN;
HANDLER tabela READ jakis_indeks='jakas wartosc';

Efektem powyższego jest zwrócenie zawartości rekordu, w przypadku którego kolumna indeksowana przez indeks „jakis_indeks” posiada wartość „jakas wartosc”. Więcej szczegółów na ten temat można oczywiście znaleźć w dokumentacji MySQL. Zgodnie z dokumentacją, HANDLER jest szybszy niż SELECT. Wiąże się to z tym, że odpada problem z koniecznością analizy i parsowania SQL, nie jest konieczny udział optimizera (bo skoro nie ma SQL, to nie bardzo co jest optymizować), nie stosuje się lockowanie tabel, tak więc odpada kolejny element do pilnowania. Sprawdźmy z resztą dokładnie jak to wygląda w praktyce:

mysql> RESET QUERY CACHE;
Query OK, 0 rows affected (0.00 sec)

mysql> SET PROFILING=1;
Query OK, 0 rows affected (0.00 sec)

mysql> HANDLER rra OPEN;
Query OK, 0 rows affected (0.00 sec)

mysql> HANDLER rra READ `PRIMARY`=(3);
+----+----------------------------------+--------------------------+----------------+-------+------+----------+
| id | hash                             | name                     | x_files_factor | steps | rows | timespan |
+----+----------------------------------+--------------------------+----------------+-------+------+----------+
|  3 | 6fc2d038fb42950138b0ce3e9874cc60 | Monthly (2 Hour Average) |            0.5 |    24 |  775 |  2678400 |
+----+----------------------------------+--------------------------+----------------+-------+------+----------+
1 row in set (0.00 sec)

mysql> HANDLER rra READ `PRIMARY`=(3);
+----+----------------------------------+--------------------------+----------------+-------+------+----------+
| id | hash                             | name                     | x_files_factor | steps | rows | timespan |
+----+----------------------------------+--------------------------+----------------+-------+------+----------+
|  3 | 6fc2d038fb42950138b0ce3e9874cc60 | Monthly (2 Hour Average) |            0.5 |    24 |  775 |  2678400 |
+----+----------------------------------+--------------------------+----------------+-------+------+----------+
1 row in set (0.00 sec)

mysql> HANDLER rra READ `PRIMARY`=(3);
+----+----------------------------------+--------------------------+----------------+-------+------+----------+
| id | hash                             | name                     | x_files_factor | steps | rows | timespan |
+----+----------------------------------+--------------------------+----------------+-------+------+----------+
|  3 | 6fc2d038fb42950138b0ce3e9874cc60 | Monthly (2 Hour Average) |            0.5 |    24 |  775 |  2678400 |
+----+----------------------------------+--------------------------+----------------+-------+------+----------+
1 row in set (0.00 sec)

mysql> SELECT * FROM rra WHERE id=3;
+----+----------------------------------+--------------------------+----------------+-------+------+----------+
| id | hash                             | name                     | x_files_factor | steps | rows | timespan |
+----+----------------------------------+--------------------------+----------------+-------+------+----------+
|  3 | 6fc2d038fb42950138b0ce3e9874cc60 | Monthly (2 Hour Average) |            0.5 |    24 |  775 |  2678400 |
+----+----------------------------------+--------------------------+----------------+-------+------+----------+
1 row in set (0.00 sec)

mysql> SELECT * FROM rra WHERE id=3;
+----+----------------------------------+--------------------------+----------------+-------+------+----------+
| id | hash                             | name                     | x_files_factor | steps | rows | timespan |
+----+----------------------------------+--------------------------+----------------+-------+------+----------+
|  3 | 6fc2d038fb42950138b0ce3e9874cc60 | Monthly (2 Hour Average) |            0.5 |    24 |  775 |  2678400 |
+----+----------------------------------+--------------------------+----------------+-------+------+----------+
1 row in set (0.00 sec)

mysql> SELECT * FROM rra WHERE id=3;
+----+----------------------------------+--------------------------+----------------+-------+------+----------+
| id | hash                             | name                     | x_files_factor | steps | rows | timespan |
+----+----------------------------------+--------------------------+----------------+-------+------+----------+
|  3 | 6fc2d038fb42950138b0ce3e9874cc60 | Monthly (2 Hour Average) |            0.5 |    24 |  775 |  2678400 |
+----+----------------------------------+--------------------------+----------------+-------+------+----------+
1 row in set (0.00 sec)

mysql> SET PROFILING=0;
Query OK, 0 rows affected (0.00 sec)

mysql> SHOW PROFILES;
+----------+------------+--------------------------------+
| Query_ID | Duration   | Query                          |
+----------+------------+--------------------------------+
|        1 | 0.00009500 | HANDLER rra OPEN               |
|        2 | 0.00017750 | HANDLER rra READ `PRIMARY`=(3) |
|        3 | 0.00016400 | HANDLER rra READ `PRIMARY`=(3) |
|        4 | 0.00008425 | HANDLER rra READ `PRIMARY`=(3) |
|        5 | 0.00024900 | SELECT * FROM rra WHERE id=3   |
|        6 | 0.00003000 | SELECT * FROM rra WHERE id=3   |
|        7 | 0.00002675 | SELECT * FROM rra WHERE id=3   |
+----------+------------+--------------------------------+
7 rows in set (0.00 sec)

I jeszcze jedna tabelka:

mysql> SHOW PROFILE FOR QUERY 3;
+--------------------+----------+
| Status             | Duration |
+--------------------+----------+
| starting           | 0.000057 |
| System lock        | 0.000005 |
| Table lock         | 0.000074 |
| query end          | 0.000003 |
| freeing items      | 0.000023 |
| logging slow query | 0.000002 |
| cleaning up        | 0.000002 |
+--------------------+----------+
7 rows in set (0.00 sec)

mysql> SHOW PROFILE FOR QUERY 4;
+--------------------+----------+
| Status             | Duration |
+--------------------+----------+
| starting           | 0.000024 |
| System lock        | 0.000003 |
| Table lock         | 0.000038 |
| query end          | 0.000003 |
| freeing items      | 0.000014 |
| logging slow query | 0.000001 |
| cleaning up        | 0.000002 |
+--------------------+----------+
7 rows in set (0.00 sec)

mysql> SHOW PROFILE FOR QUERY 5;
+--------------------------------+----------+
| Status                         | Duration |
+--------------------------------+----------+
| starting                       | 0.000012 |
| Waiting on query cache mutex   | 0.000002 |
| checking query cache for query | 0.000039 |
| Opening tables                 | 0.000013 |
| System lock                    | 0.000003 |
| Table lock                     | 0.000006 |
| Waiting on query cache mutex   | 0.000014 |
| init                           | 0.000028 |
| optimizing                     | 0.000012 |
| statistics                     | 0.000035 |
| preparing                      | 0.000011 |
| executing                      | 0.000003 |
| Sending data                   | 0.000030 |
| end                            | 0.000004 |
| query end                      | 0.000002 |
| freeing items                  | 0.000011 |
| Waiting on query cache mutex   | 0.000009 |
| Waiting on query cache mutex   | 0.000001 |
| storing result in query cache  | 0.000013 |
| logging slow query             | 0.000001 |
| cleaning up                    | 0.000002 |
+--------------------------------+----------+
21 rows in set (0.00 sec)

mysql> SHOW PROFILE FOR QUERY 6;
+--------------------------------+----------+
| Status                         | Duration |
+--------------------------------+----------+
| starting                       | 0.000006 |
| Waiting on query cache mutex   | 0.000001 |
| checking query cache for query | 0.000004 |
| checking privileges on cached  | 0.000003 |
| sending cached result to clien | 0.000013 |
| logging slow query             | 0.000002 |
| cleaning up                    | 0.000002 |
+--------------------------------+----------+
7 rows in set (0.00 sec)

Test był wykonywany na lokalnej bazie danych, połączenie przez socket. Jaki z powyższego wniosek? Po pierwsze, HANDLER faktycznie jest szybszy niż SELECT. Pierwsze wywołanie HANDLERa było o ok. 30% szybsze niż pierwsze wywołanie SELECTa. Porównując profil zapytania 3 i 5 widać wyraźnie dlaczego tak jest. HANDLER pomija inicjowanie zapytania, optymizowanie zapytania, sprawdzanie statystyk indeksów, wykonywanie SELECTa, przesyłanie danych i tak dalej. Tak na marginesie, w profilu HANDLERa widać nałożenie blokady na tabelę. Nie jest to zgodne z dokumentacją, ale biorąc pod uwagę że opisy stanów wątków w MySQL są dość ogólne, możliwe jest że jedno drugiego nie wyklucza i to, że jest status ‚Table lock’ nie oznacza, że faktycznie nakładany jest lock na tabelę.
Druga, bardzo istotna kwestia to to, że HANDLER nie korzysta z cache zapytań – wzrost prędkości działania to po prostu efekt systemowego cache dyskowego – dany rekord po prostu siedział już w pamięci. SELECT z cache zapytań oczywiście korzysta, co skutkuje tym, że jeśli wynik pobierany jest z cache, to SELECT jest ponad trzykrotnie szybszy od najszybszego zanotowanego wyniku w przypadku HANDLERa.

Kilka dni temu, na własne potrzeby, wykonałem trochę bardziej wiarygodny (kilkaset tysięcy powtórzeń) test szybkości działania SELECTa i HANDLERa. Tym razem wykonywany był na zdalnej maszynie, przez co dodatkowy wpływ miało opóźnienie na sieci. Wyniki były następujące: HANDLER był szybszy o ok.8% od SELECTa, jeśli SELECT nie korzystał z cache zapytań. W sytuacji gdy cache zapytań było stosowane, SELECT był szybszy o ok. 31%.

Co z tego wynika? Jedną z zalet MySQL jest spora elastyczność. Mamy możliwość wykorzystania dziesiątków różnych silników bazodanowych, niektóre pisane tylko do konkretnych, dedykowanych zastosowań. To jest dobre. Dobrym jest też to, że mamy różne możliwości dostania się do danych – w niektórych wypadkach lepszym rozwiązaniem będzie zastosowanie HANDLERa, w innych klasycznego SELECTa. Dzięki temu można uzyskać najwyższą wydajność dla danej bazy i danych zapytań, które idą do niej.