Przejdź do treści

MySQL – optymalizacja i wydajność

O pracy MySQL DBA – przemyślenia administratora

Archiwa

Kategoria: MySQL

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…

Baza danych MySQL udostępnia administratorowi kilka różnych silników, które może wykorzystać do przechowywania danych. W przypadku różnych silników różnie wygląda fizyczna struktura plików, które są zakładane na dysku. W szczególności, w InnoDB w domyślnej konfiguracji wszystkie dane wgrywane są do jednego, dużego pliku. Gdy w grę wchodzi większa ilość baz danych, trudno określić, ile danych znajduje się w każdej z nich. Na szczęście, tego typu informacje możemy uzyskać z innego źródła.

Twórcy MySQL, ku radości administratorów, zdecydowali się zaimplementować część 11 standardu ANSI/ISO SQL:2003 – Information Schema. Information Schema jest rozwiązaniem, które bardzo ułatwia administrowanie bazą danych MySQL i które pewnie nie raz będę wspominał na tym blogu. Trzeba pamiętać tylko o jednym – zapytania idące do tej meta-bazy mogą trwać długo – szczególnie gdy dany serwer MySQL serwuje dane z dużej ilości baz, dużej ilości tabel itd. Dobrze jest trzymać sobie drugie połączenie do serwera MySQL, tak aby można było monitorować sytuację i w razie potrzeby reagować.
czytaj dalej…

Na temat slowlogów dostępnych w MySQL pisałem jakiś czas temu – mowa wtedy była o logach, które są dostępne w dystrybucji MySQL udostępnianej przez twórców tego serwera. Przykładowy wpis w takim logu może wyglądać następująco:

# Time: 100505 12:57:11
# User@Host:  root[root] @ localhost []
# Query_time: 0.183091  Lock_time: 0.000101  Rows_sent: 4  Rows_examined: 3034
SET timestamp=1273057031;
SELECT  SQL_NO_CACHE * FROM film LEFT OUTER JOIN film_text USING (film_id)  ORDER BY film.title, film_text.description LIMIT 30,4;

Wpis w logu dla tego samego zapytania w przypadku stosowanej przeze mnie wersji MySQL wygląda następująco:

# Time: 100505  12:54:13
# User@Host: root[root] @ localhost []
# Thread_id: 4249   Schema: sakila
# Query_time: 0.165546  Lock_time: 0.000081   Rows_sent: 4  Rows_examined: 3034  Rows_affected: 0  Rows_read: 34
#  Bytes_sent: 2262  Tmp_tables: 1  Tmp_disk_tables: 1  Tmp_table_sizes:  1750700
# QC_Hit: No  Full_scan: Yes  Full_join: No  Tmp_table: Yes   Tmp_table_on_disk: Yes
# Filesort: Yes  Filesort_on_disk: No   Merge_passes: 0
#   InnoDB_IO_r_ops: 0  InnoDB_IO_r_bytes: 0   InnoDB_IO_r_wait: 0.000000
#   InnoDB_rec_lock_wait: 0.000000   InnoDB_queue_wait: 0.000000
#   InnoDB_pages_distinct: 12
SET  timestamp=1273056853;
SELECT SQL_NO_CACHE * FROM film LEFT OUTER JOIN  film_text USING (film_id) ORDER BY film.title, film_text.description  LIMIT 30,4;

czytaj dalej…