W przypadku części serwisów konieczne jest podzielenie treści na strony. Można chcieć np. wyświetlać artykuły po 10 na stronie, może chodzić o podzielenie wyników wyszukiwania produktów w sklepie itp. Dobrze jest też mieć możliwość podać użytkownikowi, ile jest łącznie wyników – coś w rodzaju „wyświetlam 1-10 artykułów z 123”. Do tego typu sytuacji MySQL udostępnia funkcję FOUND_ROWS(), która w połączeniu z dyrektywą SQL_CALC_FOUND_ROWS umożliwia sprawdzenie ile rekordów zwraca dane zapytanie, jeśli nie będzie pod uwagę brana wartość LIMIT. Czyli wykonujemy zapytania:
SELECT FOUND_ROWS();
Pierwsze z nich zwraca treść 10 pierwszych artykułów, a także zlicza ile artykułów znajduje się w kategorii 10. Wynik tych obliczeń zwraca drugie zapytanie, które już nie odwołuje się do danych w tabeli. Teoretycznie te dwa zapytania można zastąpić dwoma następującymi:
SELECT COUNT(*) FROM artykul WHERE kategoria=10;
Które rozwiązanie jest szybsze? Sprawdźmy. Utworzymy trzy tabele, o identycznej strukturze.
*************************** 1. row ***************************
Table: tab1
Create Table: CREATE TABLE `tab1` (
`a` int(10) NOT NULL AUTO_INCREMENT,
`b` int(10) NOT NULL,
`c` int(10) NOT NULL,
`d` varchar(32) NOT NULL,
PRIMARY KEY (`a`),
KEY `bc` (`b`,`c`)
) ENGINE=MyISAM AUTO_INCREMENT=10000001 DEFAULT CHARSET=latin2
1 row in set (0.00 sec)
Do każdej wrzuconych zostało 10 milionów rekordów. Sprawdźmy jak to wygląda w praktyce.
+-------+-----+----+----------------------------------+------+------+----------------------------------+
| a | b | c | d | b | c | d |
+-------+-----+----+----------------------------------+------+------+----------------------------------+
| 34223 | 222 | 2 | 01f11b1dc7251b4dad589e664e28aaf6 | 222 | 6 | 01f11b1dc7251b4dad589e664e28aaf6 |
| 14223 | 222 | 8 | 164f545c22e17e5e9298b1c84b9e3e1e | 222 | 1 | 164f545c22e17e5e9298b1c84b9e3e1e |
| 21223 | 222 | 6 | 2d5b1c6a842b1f669d976472cc52e371 | 222 | 4 | 2d5b1c6a842b1f669d976472cc52e371 |
| 37223 | 222 | 0 | 2f861ba9e11c7e15810214b1d17c4802 | 222 | 1 | 2f861ba9e11c7e15810214b1d17c4802 |
| 9223 | 222 | 1 | 497d0b20f66cebdedc7935e3ffd46efa | 222 | 5 | 497d0b20f66cebdedc7935e3ffd46efa |
| 8223 | 222 | 3 | 532923f11ac97d3e7cb0130315b067dc | 222 | 1 | 532923f11ac97d3e7cb0130315b067dc |
| 17223 | 222 | 10 | 5e38d982aecea3f5fa249828e8f1548a | 222 | 1 | 5e38d982aecea3f5fa249828e8f1548a |
| 15223 | 222 | 7 | 7d4a769562e3528950e2d1aebdfb0550 | 222 | 2 | 7d4a769562e3528950e2d1aebdfb0550 |
| 2223 | 222 | 9 | 934b535800b1cba8f96a5d72f72f1611 | 222 | 2 | 934b535800b1cba8f96a5d72f72f1611 |
| 223 | 222 | 5 | bcbe3365e6ac95ea2c0343a2395834dd | 222 | 7 | bcbe3365e6ac95ea2c0343a2395834dd |
+-------+-----+----+----------------------------------+------+------+----------------------------------+
10 rows in set (0.12 sec)
mysql> SELECT FOUND_ROWS();
+--------------+
| FOUND_ROWS() |
+--------------+
| 11 |
+--------------+
1 row in set (0.00 sec)
Jak widać, zamykamy się w 0,12 sekundy. Rozwiązanie wykorzystujące COUNT(*) wyglądałoby następująco:
+-------+-----+----+----------------------------------+------+------+----------------------------------+
| a | b | c | d | b | c | d |
+-------+-----+----+----------------------------------+------+------+----------------------------------+
| 34223 | 222 | 2 | 01f11b1dc7251b4dad589e664e28aaf6 | 222 | 6 | 01f11b1dc7251b4dad589e664e28aaf6 |
| 14223 | 222 | 8 | 164f545c22e17e5e9298b1c84b9e3e1e | 222 | 1 | 164f545c22e17e5e9298b1c84b9e3e1e |
| 21223 | 222 | 6 | 2d5b1c6a842b1f669d976472cc52e371 | 222 | 4 | 2d5b1c6a842b1f669d976472cc52e371 |
| 37223 | 222 | 0 | 2f861ba9e11c7e15810214b1d17c4802 | 222 | 1 | 2f861ba9e11c7e15810214b1d17c4802 |
| 9223 | 222 | 1 | 497d0b20f66cebdedc7935e3ffd46efa | 222 | 5 | 497d0b20f66cebdedc7935e3ffd46efa |
| 8223 | 222 | 3 | 532923f11ac97d3e7cb0130315b067dc | 222 | 1 | 532923f11ac97d3e7cb0130315b067dc |
| 17223 | 222 | 10 | 5e38d982aecea3f5fa249828e8f1548a | 222 | 1 | 5e38d982aecea3f5fa249828e8f1548a |
| 15223 | 222 | 7 | 7d4a769562e3528950e2d1aebdfb0550 | 222 | 2 | 7d4a769562e3528950e2d1aebdfb0550 |
| 2223 | 222 | 9 | 934b535800b1cba8f96a5d72f72f1611 | 222 | 2 | 934b535800b1cba8f96a5d72f72f1611 |
| 223 | 222 | 5 | bcbe3365e6ac95ea2c0343a2395834dd | 222 | 7 | bcbe3365e6ac95ea2c0343a2395834dd |
+-------+-----+----+----------------------------------+------+------+----------------------------------+
10 rows in set (0.13 sec)
mysql> SELECT SQL_NO_CACHE COUNT(*) as total FROM (SELECT a FROM tab1 LEFT OUTER JOIN tab2 USING (a) WHERE tab1.b=222 AND a != 5223 GROUP BY tab1.c) as zzz;
+-------+
| total |
+-------+
| 11 |
+-------+
1 row in set (0.10 sec)
W drugim przypadku, ze względu na GROUP BY, konieczne jest wykonanie COUNT(*) na podzapytaniu, dopiero wtedy w wyniku uzyskujemy ilość rekordów. Jak widać, łącznie zajęło to 0,23 sekundy, czyli prawie dwukrotnie więcej niż pierwsze rozwiązanie.
Sprawdźmy teraz, jak sprawa wygląda w przypadku prostszego zapytania:
+--------+-----+---+----------------------------------+
| a | b | c | d |
+--------+-----+---+----------------------------------+
| 37223 | 222 | 0 | 2f861ba9e11c7e15810214b1d17c4802 |
| 50223 | 222 | 0 | 9407c90c49bf04f8b59677647ef56522 |
| 52223 | 222 | 0 | cfe9d26390606405f1a2d2095a8fea96 |
| 106223 | 222 | 0 | 93afab528f766cb76bb911671b0f98c4 |
| 110223 | 222 | 0 | 477d4b7918c7fd5f544af3192976fb24 |
| 112223 | 222 | 0 | 7e2dc464248a306c5e43ca48e9d9488b |
| 129223 | 222 | 0 | c23a90854bea7fe90a4a80e2aa34cb54 |
| 135223 | 222 | 0 | 95d7952c87bf12e9ba419498c09f9b27 |
| 158223 | 222 | 0 | 3dc9243a4fa705a75fbb685991d04c7a |
| 181223 | 222 | 0 | a3d41682b4bbd484eeef0bb40e3e07d4 |
+--------+-----+---+----------------------------------+
10 rows in set (0.00 sec)
mysql> SELECT SQL_NO_CACHE COUNT(*) FROM tab1 WHERE b=222;
+----------+
| COUNT(*) |
+----------+
| 10000 |
+----------+
1 row in set (0.00 sec)
Jak widać, oba zapytania wykonują się poniżej 0,01 sekundy. W przypadku użycia SQL_CALC_FOUND_ROWS wygląda to następująco:
+--------+-----+---+----------------------------------+
| a | b | c | d |
+--------+-----+---+----------------------------------+
| 37223 | 222 | 0 | 2f861ba9e11c7e15810214b1d17c4802 |
| 50223 | 222 | 0 | 9407c90c49bf04f8b59677647ef56522 |
| 52223 | 222 | 0 | cfe9d26390606405f1a2d2095a8fea96 |
| 106223 | 222 | 0 | 93afab528f766cb76bb911671b0f98c4 |
| 110223 | 222 | 0 | 477d4b7918c7fd5f544af3192976fb24 |
| 112223 | 222 | 0 | 7e2dc464248a306c5e43ca48e9d9488b |
| 129223 | 222 | 0 | c23a90854bea7fe90a4a80e2aa34cb54 |
| 135223 | 222 | 0 | 95d7952c87bf12e9ba419498c09f9b27 |
| 158223 | 222 | 0 | 3dc9243a4fa705a75fbb685991d04c7a |
| 181223 | 222 | 0 | a3d41682b4bbd484eeef0bb40e3e07d4 |
+--------+-----+---+----------------------------------+
10 rows in set (0.02 sec)
mysql> SELECT FOUND_ROWS();
+--------------+
| FOUND_ROWS() |
+--------------+
| 10000 |
+--------------+
1 row in set (0.00 sec)
Oba zapytania trwają łącznie 0,02 sekundy. Różnica jest, jak widać, znaczna.
Jaki z powyższego wniosek? Każde zapytanie trzeba sprawdzić na konkretnej bazie, w konkretnej konfiguracji i z konkretnym zestawem danych. Nie da się jednoznacznie określić, czy szybsze jest rozwiązanie z SQL_CALC_FOUND_ROWS czy też lepiej jest jednak wykonać osobno SELECT, a osobno SELECT COUNT(*). Teoretycznie, na podstawie powyższych wyników można by stwierdzić, że w przypadku bardziej skomplikowanych zapytań (JOIN, GROUP BY itp.) szybsze jest zastosowanie SQL_CALC_FOUND_ROWS, a w przypadku zapytań prostych, gez grupowań, JOINów i innych takich atrakcji – SELECT COUNT(*). Tylko że to wcale nie musi być prawda. Może się okazać, że na inaczej skonfigurowanym serwerze bazodanowym wyniki będą zupełnie inne (i zasadniczo tak jest – jeśli pogooglamy za tym problemem, natkniemy się na kompletnie sprzeczne stwierdzenia).
Jedyne, co mogę napisać, jest to, że podczas podejmowania decyzji co do struktury bazy, rodzaju zapytania itp. podstawą jest benchmarkowanie. Nawet zwykłe odpalenie różnych zapytań po tysiąc razy w pętli i sprawdzenie jak długo się ta pętla wykonuje, da już jakieś pojęcie o tym, które z nich jest szybsze. Profiling i polecenie EXPLAIN pozwolą zrozumieć co się w zapytaniu dzieje i czym jedno różni się od drugiego. Niestety, ku rozpaczy części użytkowników, którzy chcieliby jednoznacznej odpowiedzi, w przypadku MySQL bardzo często jedyną słuszną odpowiedzią na pytanie „co jest lepsze”, jest – to zależy, sprawdź to sam, na swoim serwerze.
Komentarze