Programiści często zadają mi to samo pytanie: „Moje API działa wolno. Od czego mam zacząć?”. Refleksem jest zazwyczaj ulepszenie serwera lub podwojenie pamięci RAM. To kosztuje pieniądze i rzadko rozwiązuje przyczynę problemu. W większości aplikacji Laravel wąskie gardło znajduje się w warstwie bazy danych. Elegancka składnia frameworka sprawia, że łatwo zapomnieć, iż każde wywołanie Eloquent ostatecznie staje się zapytaniem SQL, a to właśnie w SQL często zaczynają się problemy.

Zanim dotkniesz ustawień serwera, metodycznie przeanalizuj swoje zapytania.

Zacznij od właściwej diagnostyki

Nie optymalizuj po omacku. Losowe przepisywanie zapytań to zgadywanie, a zgadywanie marnuje godziny.

Musisz znaleźć instrukcje, które zużywają najwięcej czasu całkowitego. Obserwuj swoją aplikację pod realnym obciążeniem. Laravel Telescope zapewnia przejrzysty widok każdego zapytania wykonanego podczas żądania, wraz z czasem jego trwania. Laravel Debugbar wyświetla je w przeglądarce podczas lokalnego rozwoju, dzięki czemu możesz natychmiast wykryć anomalie. Gdy musisz wyłapać problemy na produkcji, włącz MySQL Slow Query Log. Rejestruje on instrukcje przekraczające zdefiniowany przez Ciebie próg, co czyni go idealnym do znajdowania niespodzianek, które nie pojawiają się w małych zbiorach danych. Jeśli uruchamiasz coś większego, narzędzie typu Application Performance Monitoring może powiązać wolne punkty końcowe HTTP z konkretnymi wywołaniami bazy danych.

Analizując dane, szukaj dwóch rzeczy: bezwzględnego czasu wykonania oraz częstotliwości wywołań. Zapytanie trwające czterdzieści milisekund brzmi niewinnie, dopóki nie uświadomisz sobie, że wykonuje się ono dwa tysiące razy na minutę. Raport trwający trzy sekundy, który uruchamia się raz na godzinę, może mieć mniejsze znaczenie niż wyszukiwanie trwające pół sekundy, które odbywa się na każdej stronie. Najpierw napraw problemy o największym wpływie.

Przestań prosić o wszystko

SELECT * jest wygodne. Jest też kosztowne. Gdy piszesz Model::all() lub pobierasz zestaw wyników bez podawania nazw kolumn, MySQL musi przeciągnąć każdą dziedzinę dla każdego pasującego wiersza. Obejmuje to duże pola tekstowe, obiekty JSON i wszystko inne znajdujące się w tabeli. Zestaw wyników rośnie, zużycie pamięci wzrasta, a czas poświęcony na serializację odpowiedzi zwiększa się.

Bądź precyzyjny. Jeśli Twój kontroler potrzebuje tylko pól id, name i email, poproś dokładnie o nie:

User::select('id', 'name', 'email')->get();

W query builderze obowiązuje ta sama zasada. Mniejsze ładunki danych przesyłane są szybciej przez sieć i zużywają mniej pamięci RAM na serwerze aplikacji. To jeden z najtańszych możliwych zysków, a mimo to łatwo o nim zapomnieć, ponieważ Laravel traktuje SELECT * jako zachowanie domyślne.

Pozwól, aby EXPLAIN prowadził Twoje zmiany

Nigdy nie refaktoryzuj wolnego zapytania bez wcześniejszego uruchomienia EXPLAIN. W MySQL słowo kluczowe EXPLAIN pokazuje plan wykonania zapytania. Ujawnia ono dokładnie, w jaki sposób optymalizator zamierza znaleźć Twoje dane.

Zwróć uwagę na kolumnę type. Jeśli widzisz ALL, MySQL wykonuje pełne skanowanie tabeli (full table scan). Oznacza to, że czyta każdy wiersz, aby spełnić warunek klauzuli WHERE. Spójrz na kolumnę key, aby zobaczyć, czy optymalizator w ogóle używa indeksu. Następnie sprawdź kolumnę Extra. Jeśli zauważysz Using temporary lub Using filesort, MySQL tworzy tabele pośrednie lub sortuje w pamięci, ponieważ obecna struktura nie pozwala na sprawne wykonanie zapytania.

Uruchom EXPLAIN w swoim kliencie MySQL lub użyj narzędzia, które sformatuje wynik dla Ciebie. Gdy zobaczysz plan, będziesz wiedzieć, czy problemem jest brakujący indeks, złe połączenie (join) czy predykat, którego silnik nie może zoptymalizować. Zgadywanie staje się zbędne.

Indeksuj z intencją

Indeksy są najpotężniejszym narzędziem przyspieszającym wyszukiwanie, ale działają tylko wtedy, gdy pasują do sposobu, w jaki formułujesz zapytania. Bez odpowiedniego indeksu MySQL skanuje tabelę wiersz po wierszu. W środowisku deweloperskim na tabeli z tysiącem wierszy może to wydawać się w porządku, ale w produkcji na tabeli z dziesięcioma milionami wierszy może doprowadzić do awarii.

Zacznij od indeksów jednokolumnowych dla pól, które często pojawiają się w klauzulach WHERE. Jeśli stale filtrujesz po status, dodaj indeks na status.

Gdy zapytanie filtruje po wielu kolumnach jednocześnie, przejdź do indeksów złożonych (composite indexes). Kolejność kolumn wewnątrz indeksu ma znaczenie, ponieważ MySQL czyta indeksy złożone od lewej do prawej. Nazywa się to zasadą lewego prefiksu (leftmost prefix rule). Jeśli Twoje zapytanie wyszukuje według user_id, a następnie sortuje według created_at, indeks złożony na (user_id, created_at) przyniesie znaczną pomoc. Odwróć kolejność, a optymalizator może w ogóle nie użyć indeksu do filtrowania.

Nie indeksuj każdej kolumny. Każdy indeks dodaje narzut przy operacjach insert, update i delete, ponieważ MySQL musi utrzymywać jego strukturę. Dodawaj je świadomie, opierając się na wzorcach wykrytych podczas pomiarów.

Trzymaj funkcje z dala od kolumn

Ten błąd po cichu wyłącza indeksy. Gdy opakujesz kolumnę w funkcję wewnątrz klauzuli WHERE, MySQL nie może