Przejdź do treści

MySQL – optymalizacja i wydajność

O pracy MySQL DBA – przemyślenia administratora

Archiwa

Archiwa z daty Czerwiec, 2010

Prawdopodobnie wszyscy administratorzy MySQL widzieli w swoich slowlogach zapytania typu:

SELECT id, to, to, tamto,  opis FROM produkt WHERE opis LIKE '%znakomity%' AND opis LIKE  '%genialny%' ORDER BY id ASC;

Zapytania tego typu potrafią się wykonywać bardzo długo, szczególnie gdy zawartość bazy jest spora. Nie korzystają też z indeksów, co jest akurat oczywiste. Aby zadziałał indeks typu BTREE, z lewej strony nie może występować wildcard. Możemy szukać pól zaczynających się w jakiś sposób: WHERE opis LIKE ‚genialny%’, ale wyrazu w środku zdania przy pomocy indeksu nie znajdziemy. Co można zrobić, aby przyspieszyć działanie takich zapytań?
czytaj dalej…

Jeden z czytelników zapytał się, jak wygląda sprawa z wydajnością ograniczeń wprowadzanych w kluczach obcych. Zgodnie z dokumentacją, InnoDB umożliwia zdefiniowania dla klucza obcego ograniczeń typu ON UPDATE CASDADE czy też ON DELETE CASCADE. Są to mechanizmy bardzo wygodne dla projektanta bazy danych, gdyż dzięki nim baza do pewnego stopnia sama z siebie pilnuje integralności danych. W przypadku dwóch tabel połączonych takim kluczem obcym, skasowanie lub modyfikacja rekordu w pierwszej skutkować będzie odpowiednią operacją na rekordzie w drugiej tabeli. Odciążamy w ten sposób aplikację, programiści nie muszą samodzielnie dbać o integralność danych. Czy jest jednak jakiś dodatkowy koszt wprowadzenia takiego ułatwienia? Do tej pory to zagadnienie nie leżało w kręgu moich zainteresowań, ale przecież można to prosto sprawdzić. Poniżej zaprezentuję to, w jaki sposób. Uogólniając, ten sam sposób postępowania można wykorzystać do sprawdzenia różnicy w wydajności dowolnej, odpowiadającej sobie pary zapytań.

czytaj dalej…

Przeszukajmy polski fragment sieci na występowanie słów kluczowych: mysql „order by rand”. Co widzimy? Na pierwszym miejscu mamy link wyjaśniający, jak najprościej pobrać losowe rekordy z tabeli. Spora część innych linków także opisuje stosowanie tej funkcji jako wygodne rozwiązanie problemu losowości. Z doświadczenia zdobytego w pracy jako administrator MySQL wiem natomiast, że ORDER BY RAND to zło. Czy można wobec tego wierzyć sieci?

Podstawową sprawą jest własnoręczne przetestowanie wszelkich sugestii, nowinek i porad znalezionych w sieci. Przetestowanie na kopii zestawu danych, który jest używany produkcyjnie (na przykład na danych z kopii zapasowej). Przetestowanie na serwerze MySQL w tej samej wersji i tak samo skonfigurowanym jak serwer produkcyjny. Przetestowanie na platformie sprzętowej jak najbliższej platformie produkcyjnej. Trzeba także zrozumieć, co się dzieje na serwerze. Jak wygląda plan dotychczasowego zapytania a jak wygląda plan zapytania zmodyfikowanego? Co się zmieniło? Co się dzieje na serwerze podczas wykonywania tych zapytań? Ile rekordów jest odczytywanych, ile zapisywanych? Ile tablic tymczasowych jest tworzonych i gdzie? Trzeba przeprowadzić benchmark – sprawdzić maksymalną ilość starych zapytań, jakie wykonają się w czasie sekundy i porównać do ilości nowych.
Dopiero po takiej analizie możemy ocenić czy dana sugestia znaleziona w Google ma sens dla naszego zestawu danych obsługiwanego przez MySQL skonfigurowany przez nas i działający na naszym serwerze fizycznym.
czytaj dalej…

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 SQL_CALC_FOUND_ROWS * FROM  artykul WHERE kategoria=10 LIMIT 10;
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  * FROM artykul WHERE kategoria=10 LIMIT 10;
SELECT COUNT(*) FROM  artykul WHERE kategoria=10;

Które rozwiązanie jest szybsze? Sprawdźmy. Utworzymy trzy tabele, o identycznej strukturze.

czytaj dalej…

Maszyna fizyczna zaczyna wykazywać ogromne zapotrzebowanie na CPU. W slowlogu pojawia się taki, bądź podobny (patrz wpisy dotyczące formatu slowlogów: standardowy i rozszerzony) wpis:

# Time: 100517 13:25:07
# User@Host: root[root] @ localhost []
# Thread_id: 19484  Schema: test
# Query_time: 0.615715  Lock_time: 0.000084  Rows_sent: 0  Rows_examined: 1497474  Rows_affected: 0  Rows_read: 1497474
# Bytes_sent: 411  Tmp_tables: 0  Tmp_disk_tables: 0  Tmp_table_sizes: 0
# QC_Hit: No  Full_scan: Yes  Full_join: No  Tmp_table: No  Tmp_table_on_disk: No
# Filesort: No  Filesort_on_disk: No  Merge_passes: 0
# No InnoDB statistics available for this query
SET timestamp=1274095507;
SELECT * FROM links WHERE a = '3457013470' AND f = '78uhimkq3no5ekk' AND ip = '165.72.200.11' AND data > '2009-05-17, 07:27';

Bezpośrednią przyczyną wystąpienia problemu jest to, że wykonywany jest pełny skan tabeli. W rozszerzonym logu jest to napisane wprost, można też wywnioskować to z faktu, że wartości Rows_examined i Rows_read są takie same. Explain potwierdza brak indeksu:

mysql> EXPLAIN SELECT * FROM links WHERE a = '3457013470' AND f = '78uhimkq3no5ekk' AND ip = '165.72.200.11' AND data > '2009-05-17, 07:27';
+----+-------------+-------+------+---------------+------+---------+------+---------+-------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows    | Extra       |
+----+-------------+-------+------+---------------+------+---------+------+---------+-------------+
|  1 | SIMPLE      | links | ALL  | NULL          | NULL | NULL    | NULL | 1497474 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+---------+-------------+
1 row in set (0,00 sec)

czytaj dalej…

Zauważyłem dziś, że przez ostatnie kilka dni mogły występować problemy z formularzem kontaktowym. Obecnie przyczyna błędów jest już wyeliminowana, maile powinny się poprawnie wysyłać. Jeśli ktoś z szanownych Czytelników chciał się ze mną skontaktować i natknął się wtedy na problem, bardzo proszę spróbować jeszcze raz. Tym razem powinno udać się to bez problemu.

Przy okazji, chciałbym przypomnieć wszystkim administratorom MySQL, że 20 maja, wraz z wersją 5.1.47 i 5.0.91 poprawione zostały trzy błędy bezpieczeństwa. http://forums.mysql.com/read.php?30,368527,368527

Najwyższa pora aby dokonać upgrade serwera, bądź też załatać obecną wersję.

nginx wp super
nginx wp super
nginx wp super

Jak wiadomo, podstawą wydajnej pracy bazy danych są odpowiednio dobrane indeksy. Niestety, to nie wystarczy. Bardzo częstym błędem, popełnianym nawet przez doświadczonych programistów, jest stosowanie w warunku WHERE funkcji działającej na wartości kolumny. O co dokładnie chodzi ilustruje podany niżej przykład. Zakładamy tabelę o następującej strukturze:

mysql> SHOW CREATE TABLE tab1\G
*************************** 1. row ***************************
Table: tab1
Create Table: CREATE TABLE `tab1` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`timestamp` int(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=3102 DEFAULT CHARSET=latin2
1 row in set (0,00 sec)

Kolumna `timestamp`, jak sama nazwa wskazuje, przechowuje datę w postaci uniksowego timestampa. Załóżmy, że w wyniku zapytania chcemy dostać tylko te rekordy, których timestamp jest późniejszy niż 2 stycznia 2004 roku. Sprawdzamy jak wygląda plan takiego zapytania:

mysql> EXPLAIN SELECT * FROM tab1 WHERE FROM_UNIXTIME(timestamp) >= '2004-01-02 00:00:00';
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows | Extra       |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
|  1 | SIMPLE      | tab1  | ALL  | NULL          | NULL | NULL    | NULL | 3101 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
1 row in set (0,00 sec)

czytaj dalej…

Zgodnie z dokumentacją MySQL JOIN w formie:

SELECT kol11, kol12, kol21, kol22 FROM tab1, tab2, tab3 WHERE tab1.id_1 = tab2.id_1 AND tab1.id_1 = tab3.id_1;

jest tożsame z zapytaniem w formie:

SELECT kol11, kol12, kol21, kol22 FROM tab1 JOIN tab2 USING (id_1) JOIN tab3 USING (id_1);

Sprawdzamy (dane są identyczne jak w przypadku poprzedniego posta):

mysql> SELECT kol11, kol12, kol21, kol22 FROM tab1, tab2, tab3 WHERE tab1.id_1 = tab2.id_1 AND tab1.id_1 = tab3.id_1;
+-----------------+-----------------+-----------------+-----------------+
| kol11           | kol12           | kol21           | kol22           |
+-----------------+-----------------+-----------------+-----------------+
| dcydbdbzdydazcz | cyzycdbcybzdczy | bcdybzdbczbcbzy | bdcybydybybyzcb |
| cbydydyaydcbzby | ydyzbdcdbyczbyd | cbcybdczbcbzyzd | dbzdcbzbdybdydz |
+-----------------+-----------------+-----------------+-----------------+
2 rows in set (0.00 sec)

mysql> SELECT kol11, kol12, kol21, kol22 FROM tab1 JOIN tab2 USING (id_1) JOIN tab3 USING (id_1);
+-----------------+-----------------+-----------------+-----------------+
| kol11           | kol12           | kol21           | kol22           |
+-----------------+-----------------+-----------------+-----------------+
| dcydbdbzdydazcz | cyzycdbcybzdczy | bcdybzdbczbcbzy | bdcybydybybyzcb |
| cbydydyaydcbzby | ydyzbdcdbyczbyd | cbcybdczbcbzyzd | dbzdcbzbdybdydz |
+-----------------+-----------------+-----------------+-----------------+
2 rows in set (0.00 sec)

czytaj dalej…