Przejdź do treści

MySQL – optymalizacja i wydajność

O pracy MySQL DBA – przemyślenia administratora

Archiwa

Archiwa z daty Maj, 2010

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…

Czasami zdarza się, że zamiast oczekiwanego efektu wykonanie SELECT’a kończy się poniższym błędem:

1104 - The SELECT would examine more than MAX_JOIN_SIZE rows; check your WHERE and use SET SQL_BIG_SELECTS=1 or SET SQL_MAX_JOIN_SIZE=# if the SELECT is okay</span>

Dzieje się tak wtedy, gdy optimizer MySQL uzna, że dane zapytanie musi sprawdzić większą ilość rekordów niż wartość zmiennej max_join_size. Zmienna ta ma na celu zabezpieczenie serwera przed wykonaniem zapytań, które trwałyby kosmicznie długo. Domyślnie przyjmuje wartość 4G (4*1024*1024*1024). Wydaje się to dużo, ale w praktyce okazuje się, że taki limit prosto przekroczyć.

Tworzymy następujące tabele:

CREATE DATABASE test;
CREATE TABLE `test`.`tab1` (id_1 INT AUTO_INCREMENT, kol11 VARCHAR(20), kol12 VARCHAR(20), PRIMARY KEY (id_1));
CREATE TABLE `test`.`tab2` (id_1 INT AUTO_INCREMENT, id_2 INT, kol21 VARCHAR(20), kol22 VARCHAR(20), PRIMARY KEY (id_1));
CREATE TABLE `test`.`tab3` (id_1 INT AUTO_INCREMENT, id_2 INT, kol31 VARCHAR(20), kol32 VARCHAR(20), PRIMARY KEY (id_1));

czytaj dalej…

Jak na razie, ruch na blogu jest na tyle niski, że jest możliwość przeanalizowania tego, o co się pytają poszczególni goście, którzy trafili na tę stronę za pomocą Google. Dziś odpowiedź, mam nadzieję że przydatna, na jedno z takich pytań. Gdyby ktoś potrzebował informacji na jakiś temat, zapraszam bezpośrednio do mnie, do zakładki Kontakt. Chętnie się dowiem, na jakiego typu posty jest zapotrzebowanie.

Co pewien czas w wyniku zapytania EXPLAIN, w kolumnie Extra pojawia się wpis: „Using filesort”. Co to dokładnie znaczy?

Nazwa filesort jest, mówiąc delikatnie, niefortunna. W praktyce, filesort nie ma nic wspólnego z sortowaniem na dysku. Jest to po prostu sortowanie wyników, które może odbywać się z wykorzystaniem dysku, ale wcale nie musi. MySQL, jeśli tylko jest w stanie, podczas sortowania wyników zapytania stara się wykorzystać indeksy. Jeśli nie jest w stanie skorzystać z żadnego – stosowany jest właśnie filesort. Filesort to w praktyce dwa różne algorytmy, oparte na algorytmie quicksort. Starszy, dwuprzebiegowy z nich ma mniejsze zapotrzebowanie na pamięć, przez co większa ilość rekordów mieści się w buforze sortowania, generuje natomiast znacznie większe obciążenie dysku losowymi operacjami odczytu i zapisu. Nowszy, dostępny od MySQL 4.1, ma większe zapotrzebowanie na pamięć, natomiast nie wymaga takiej ilości operacji dyskowych. Administrator MySQL ma pewne możliwości sterowania tym, jaki algorytm ma być stosowany. Jeśli wielkość zestawu danych do sortowania przekracza wartość zmiennej konfiguracyjnej max_length_for_sort_data, stosowany jest filesort dwuprzebiegowy, tak więc zwiększając wartość tej zmiennej zwiększamy preferencję stosowania nowszego, jednoprzebiegowego algorytmu.

Jeśli zestaw danych do sortowania jest większy niż wartość zmiennej sort_buffer_size, dane do sortowania dzielone są na mniejsze części, a następnie łączone na dysku. Nie jest to jednak powodem (powtarzam, nie jest to powodem!) aby zwiększać wartość tej zmiennej. Akurat ta zmienna jest doskonałym przykładem na to, że więcej nie znaczy lepiej. Są przypadki, w których zwiększenie buforu zwiększa wydajność. Są też przypadki, kiedy dzieje się zupełnie na odwrót, a w których najlepszym rozwiązaniem jest zmniejszenie bufora. Wiąże się to po części ze sposobem alokacji pamięci przez bibliotekę libc – przy pewnej wielkości (pewien czas temu było to 256KB) zmienia się funkcja służąca do alokacji pamięci z malloc() na mmap(). Każdej zmianie konfiguracji musi towarzyszyć dokładne przetestowanie szybkości wykonywania zapytań, tak aby mieć pewność, że faktycznie zmiany przyniosły oczekiwany efekt.
czytaj dalej…

MySQL udostępnia administratorowi kilka rodzajów logów – error log, slowlog, general log, binlog. W sytuacji, gdy pojawiają się problemy wydajnościowe i konieczne jest przeanalizowanie obciążenia i znalezienie przyczyny kłopotów, przydatny staje się głównie jeden z nich – slowlog. Log ten zawiera informacje na temat zapytań, które wykonywały się dłużej niż wartość zmiennej ‚long_query_time’ w sekundach. Domyślnie jest to 10 sekund. Zazwyczaj domyślna wartość jest dużo za duża – kilkadziesiąt zapytań, z których każde wykonuje się po kilka sekund, może spokojnie zatkać serwer bazodanowy. Z tego powodu, jak również z pewnych historycznych względów, często ustawia się ten parametr na jedną sekundę.
W przypadku MySQL w wersji 5.1 przykładowy wpis ze slowloga może wyglądać następująco:

# Time: 100427  20:55:52
# User@Host: root[root] @ localhost []
# Query_time:  0.000274  Lock_time: 0.000075 Rows_sent: 5  Rows_examined: 5
SET  timestamp=1272394552;
select * from mysql.user;

Co widzimy?
– czas, kiedy zapytanie zostało wykonane: 27 kwietnia 2010 roku o godzinie 20:55:52
– jaki użytkownik i z jakiego hosta wykonał zapytanie: użytkownik root logujący się lokalnie, po sockecie (localhost)
– ile trwało wykonanie zapytania: 0.000274 sekundy
– jak długo tabela była blokowana: 0.000075 sekundy
– ile rekordów zostało przesłanych do klienta: 5
– ile rekordów zostało sprawdzonych w bazie: 5
– zapytanie, jakie zostało wykonane

czytaj dalej…

Zastosowanie ORDER BY RAND() generuje tabelę tymczasową. Zawsze. Administratorzy baz danych nie lubią tabel tymczasowych, bo powodują że zapytanie działa wolno. Niestety, osoby piszące aplikacje korzystające z bazy MySQL często są nieświadome tego, jak dużym problemem może być zastosowanie tego typu sortowania wyników.
Użycie ORDER BY RAND() wymaga od bazy danych utworzenia tabeli tymczasowej z wynikiem zapytania, przydzielenia wszystkim rekordom losowych współczynników, po których są one następnie sortowane. Jeśli tabela tymczasowa miałaby mieć wielkość do kilkuset rekordów, nie stanowi to jeszcze problemu. Problem pojawia się natomiast w momencie, gdy ilość rekordów przekracza kilka tysięcy i pogłębia się wraz ze wzrostem wielkości tabeli.

Jak można zastąpić zastosowanie ORDER BY RAND()? Zależy to od tego, o jakie konkretnie zapytanie chodzi. Jeśli chcemy wyciągnąć pojedynczy, losowy rekord, to względnie  optymalnym rozwiązaniem, ale wymagającym istnienia w tabeli jakiejś kolumny z unikalnymi identyfikatorami rekordów będzie:

SELECT  MAX(kol_id) FROM tabela;

Z przedziału od 0 do wyniku powyższego zapytania generujemy losową liczbę (X), a następnie próbujemy znaleźć jakiś rekord o id do niej zbliżonym.

SELECT * FROM  tabela WHERE kol_id >= X LIMIT 1;

czytaj dalej…