Baza sklepu bez indeksów: każdy filtr i JOIN schodził do pełnego skanu, aż właściwe indeksy dały mikrosekundy

Baza sklepu bez indeksów działała, dopóki tabele były małe. Przy 500 tysiącach zamówień i 1,75 miliona pozycji każdy filtr i każdy JOIN schodził do pełnego skanu. Zmierzyliśmy dokładnie, ile kosztują brakujące indeksy, dodaliśmy właściwe i opisaliśmy pułapki, przez które indeks bywa bezużyteczny, choć istnieje.

  • JOIN trzech tabel: 406 ms, z indeksem 0,075 ms
  • Filtr zamówień: 500 000 skanowanych wierszy, z indeksem 5
  • Indeks obecny, a mimo to pomijany: pokazujemy kiedy i dlaczego
Tabela z dwunastu wierszy; po lewej długa strzałka biegnie przez wszystkie wiersze pod podpisem ALL z liczbą 500 000 poniżej, po prawej karta indeksu wysyła jedną pomarańczową strzałkę do pojedynczego wiersza pod podpisem ref z liczbą 5 poniżej

Baza sklepu internetowego na MariaDB: klienci, produkty, zamówienia, pozycje zamówień, raporty i historia klienta

Wyzwanie

Baza sklepu zaprojektowana w pośpiechu miała tylko klucze główne. Przy małych tabelach wszystko było natychmiastowe, więc brak indeksów nie rzucał się w oczy. Wraz ze wzrostem danych zapytania, które wcześniej trwały milisekundy, zaczęły trwać setki milisekund, a listy i raporty pod obciążeniem zajmowały bazę na długie chwile.

Zadanie: pokazać pomiarem, ile kosztuje brak indeksów na prawdziwych danych, dodać właściwe indeksy do typowych zapytań sklepu, filtra po kliencie, historii zamówień z JOIN, raportu sprzedaży i paginacji, oraz wskazać pułapki, przez które indeks jest bezużyteczny, choć został założony.

Rozwiązanie

Baza taka, jaką zastaliśmy: cztery tabele, klienci, produkty, zamówienia i pozycje, z samymi kluczami głównymi. Klucze obce między nimi istniały tylko w kodzie aplikacji, nie jako ograniczenia w bazie, więc nic nigdy nie założyło na nich indeksu. Dla każdego typowego zapytania sklepu zarejestrowaliśmy plan, liczbę wierszy odczytanych skanem i medianę czasu, potem dodaliśmy właściwe indeksy jeden po drugim i zmierzyliśmy to samo zapytanie ponownie.

Pułapki dostały osobne pomiary: przypadki, w których indeks istnieje, a baza go nie używa albo używa źle. A ponieważ indeks nie jest darmowym zyskiem, zmierzyliśmy, o ile pięć indeksów spowalnia zapis i ile zajmuje miejsca. Dane sklepu w tamtym momencie: 100 000 klientów, 50 000 produktów, 500 000 zamówień i 1,75 miliona pozycji na MariaDB 10.11 z InnoDB, stan na 2026-10.

Wyniki

Historia klienta, JOIN trzech tabel
Z 406,8 ms na 0,075 ms
Zamówienia klienta
Z 500 000 skanowanych wierszy na 5
Wyszukiwanie po adresie e-mail
Z 5,9 ms na 0,024 ms
Dziesięć najnowszych zamówień
Z 39,3 ms na 0,009 ms
Raport sprzedaży po statusie i zakresie dat
Z 34 268 wierszy na 3 336 dzięki indeksowi złożonemu
Cena pięciu indeksów na tabeli zamówień
Zapis 3,1 raza wolniejszy, indeksy to 237 procent rozmiaru tabeli

Jak pracowaliśmy

  1. Punkt wyjścia taki, jaki zastaliśmy

    Cztery tabele z samymi kluczami głównymi; klucze obce między nimi żyły w kodzie aplikacji i nie miały w bazie żadnego indeksu.

  2. Stan wyjściowy każdego zapytania

    Plan z EXPLAIN, licznik wierszy odczytanych skanem, niezależny od cache, i mediana czasu z wielu przebiegów.

  3. Właściwe indeksy, ten sam pomiar

    Indeks na każdą kolumnę w filtrze, złączeniu lub sortowaniu, zakładany jeden po drugim na bazie sklepu, potem identyczne zapytanie jeszcze raz.

  4. Pułapki zmierzone osobno

    Indeks, który istnieje i jest pomijany, funkcja na kolumnie, niezgodność typów, kolejność kolumn w indeksie złożonym, indeks pokrywający.

  5. Policzony koszt

    Miejsce na dysku i spowolnienie zapisu przy pięciu indeksach na tabeli zamówień.

  6. Checklista dla zespołu

    Które zapytania sprawdzać, które kolumny indeksować, które indeksy usunąć, bo żaden plan ich nie wybiera.

Wyzwania, które pokonaliśmy

To, że indeks przyspiesza zapytanie, nie jest ciekawe. Ciekawe jest, kiedy indeks istnieje i nie pomaga, oraz jak czytać plan, żeby to zobaczyć.

„Użyty indeks” to nie to samo co szybko

Plan pokazuje nazwę klucza nawet wtedy, gdy baza czyta cały indeks. Kolumną do czytania jest typ dostępu: ref, eq_ref i range to wyszukanie przez indeks, index to pełny skan indeksu, ALL to pełny skan tabeli. Plan potrafi podać nazwę klucza i zgłosić Using index, a pod spodem i tak czytać milion wpisów. Nazwa klucza nie jest dowodem; dowodem są typ dostępu i liczba wierszy.

Indeks istnieje, a baza go ignoruje

Na kolumnie statusu, pięć różnych wartości, założyliśmy indeks, a suma wartości opłaconych zamówień i tak wykonała pełny skan tabeli: 500 000 wierszy, 35,7 ms. To nie błąd optymalizatora, tylko decyzja: opłacone zamówienia to 20 procent tabeli, a kwota nie jest w indeksie, więc użycie indeksu oznaczałoby 100 000 losowych doskoków po wiersze, a to kosztuje więcej niż jeden sekwencyjny skan. Indeks na kolumnie o niewielu różnych wartościach często niczego nie zmienia.

Funkcja na kolumnie wyłącza indeks

Mając indeks na dacie zamówienia, dwa zapytania o ten sam dzień zachowują się zupełnie inaczej. Warunek z datą opakowaną funkcją czyta cały indeks, 500 000 wpisów, 32,7 ms. Warunek jako zakres od północy do północy czyta 556 wierszy w 0,073 ms. Różnica około 447 razy bierze się wyłącznie z funkcji wokół kolumny:

WHERE DATE(created_at) = '2026-06-15'                              pełny skan indeksu
WHERE created_at >= '2026-06-15' AND created_at < '2026-06-16'      wyszukanie zakresu

Opakowanie indeksowanej kolumny czymkolwiek, funkcją, konkatenacją, rzutowaniem, odbiera bazie wyszukanie po zakresie. Regułę pisze się po stronie wartości, nie kolumny.

Niejawna konwersja typu: zły wynik, i to wolniej

Najgroźniejsza pułapka, bo uderza w poprawność, nie tylko w szybkość. Kolumna statusu jest tekstowa. Porównana z wartością tekstową zwraca 100 000 wierszy w 8,5 ms przez indeks. Porównana z liczbą 0 zamiast tekstu zwraca wszystkie 500 000 wierszy w 84,6 ms pełnym skanem indeksu. MariaDB przy porównaniu rzutuje każdy tekst na liczbę, każdy status daje 0 i warunek pasuje do każdego wiersza. Zapytanie jest wolniejsze, zwraca zły wynik i zgłasza tylko ostrzeżenie, którego nikt nie czyta. Typy w warunku muszą się zgadzać.

Indeks złożony działa tylko od lewej

Indeks złożony na produkcie i ilości w pozycjach zamówień obsługuje warunek na produkt w 0,020 ms. Warunek na samą ilość nie może z niego skorzystać do wyszukania i skanuje cały indeks, 1,75 miliona wpisów, 96,1 ms. Indeks wielokolumnowy obsługuje warunki tylko od swojej lewej krawędzi, więc kolejność kolumn jest decyzją projektową, dobraną do prawdziwych zapytań.

Indeks pokrywający usuwa doskoki po wiersze

Lista tysiąca najnowszych zamówień z numerem i kwotą: przy indeksie na samej dacie baza czyta indeks po kolei, ale po kwotę musi doskoczyć do każdego wiersza, 0,75 ms. Przy indeksie na dacie i kwocie wszystko, czego zapytanie potrzebuje, jest w indeksie, bez doskoków, 0,24 ms. Przy większych wynikach i zimnym cache różnica jest znacznie większa niż to trzykrotne przyspieszenie.

Dwa tory: górny warunek DATE(created_at) = … kończy się krzyżykiem przed kartą indeksu; dolny warunek created_at >= … AND < … dochodzi do swojej karty indeksu pomarańczowym torem

Zmierzone bez indeksu i z indeksem

Zapytanie Bez indeksu Z indeksem Przyspieszenie
Wyszukiwanie po adresie e-mail Pełny skan, 100 000 wierszy, 5,9 ms Wyszukanie, 1 wiersz, 0,024 ms Około 245 razy
Zamówienia klienta Pełny skan, 500 000 wierszy, 24,1 ms Wyszukanie, 5 wierszy, 0,020 ms Około 1 200 razy
Historia klienta, JOIN 3 tabel Skan pozycji, 1,75 miliona wierszy, 406,8 ms Wyszukania, 0,075 ms Około 5 400 razy
Dziesięć najnowszych zamówień Pełny skan i sortowanie plikowe, 39,3 ms Odczyt z indeksu, 10 wierszy, 0,009 ms Około 4 360 razy
Raport sprzedaży, status i zakres dat Indeks na dacie, 34 268 wierszy, 9,7 ms Indeks złożony, 3 336 wierszy, 2,2 ms Około 4,4 raza
Koszt: wstawienie 200 000 zamówień 0,53 s bez indeksów 1,64 s z pięcioma indeksami, 60,6 MB indeksów przy 25,6 MB danych 3,1 raza wolniej

Co z tego wynika

  • Indeksować kolumny z warunków i złączeń. Kolumny w filtrze, w warunku złączenia i w sortowaniu to pierwsi kandydaci. Logiczny klucz obcy bez indeksu to najczęstsza przyczyna wolnych JOIN-ów.
  • Czytać plan, zwłaszcza typ dostępu. ALL i index to skany; celem jest ref, eq_ref albo wąski range. Sama nazwa klucza niczego nie dowodzi.
  • Nie owijać indeksowanych kolumn funkcjami. Przenieść operację na stronę wartości albo rozważyć kolumnę wyliczaną z własnym indeksem.
  • Pilnować typów w warunkach. Porównanie tekstu z liczbą potrafi dać zły wynik i wyłączyć indeks.
  • Dobierać kolejność kolumn w indeksie złożonym do zapytań. Działa tylko lewy prefiks.
  • Rozważać indeksy pokrywające dla gorących zapytań i ważyć je z kosztem zapisu i miejsca.
  • Indeksować z umiarem. Każdy indeks spowalnia zapis i zajmuje miejsce: pięć indeksów na tabeli zamówień zajęło 237 procent jej rozmiaru i spowolniło wstawianie 3,1 raza. Zakładać te, które są używane, usuwać te, których żaden plan nie wybiera, i mierzyć ponownie na prawdziwych danych. Ta sama dyscyplina obowiązuje po stronie aplikacji, gdzie duży wynik zapytania wciągnięty do pamięci potrafi położyć serwer.

Źródła

Na jednej linii bazowej czarny blok z podpisem 26 MB oznaczający dane tabeli zamówień i obok wyższy pomarańczowy stos pięciu bloków z podpisem 61 MB oznaczający jej indeksy

Powiązane sytuacje

  • Państwa system działa, ale nikt nie zna go dokładnie. Spisujemy aplikację, bazę danych, serwer i pocztę, oceniamy ryzyka, na piśmie rekomendujemy dalszą drogę, a na życzenie przejmujemy utrzymanie systemu.

  • Programista złożył wypowiedzenie albo już odszedł, a system musi działać. Sprawdzamy system i w ciągu 48 godzin po uzyskaniu dostępu przekazujemy Państwu pisemny raport ze stanem systemu, ryzykiem i planem stabilizacji.

Inne projekty

  • Skala punktów z przerywanym progiem: koperta phishingu przechodzi pomarańczowym łukiem przez próg

    Filtr antyspamowy, który działał, ale nie chronił

    Usługi uruchomione, konfiguracja poprawna, w statusie systemu żadnych błędów. A mimo to phishing lądował w skrzynkach odbiorczych, a newslettery partnerów w folderze spam. Nasi inżynierowie zmierzyli, które mechanizmy wykrywania naprawdę działają, usunęli przyczyny, a trzy miesiące później znaleźli i naprawili regresję, którą było widać dopiero w ujęciu godzinowym.

  • Cztery koperty przechodzą przez bramki SPF, DKIM i DMARC, odhaczone na pomarańczowo, w drodze do skrzynki odbiorczej

    Dostarczalność poczty dla czterech domen firmowych

    Serwer pocztowy czterech domen firmowych niczego nie podpisywał, nie publikował polityki DMARC i był odrzucany przez Microsoft. Nasi inżynierowie sprawdzili go, przetestowali zmianę na jednej domenie i wdrożyli ją partiami. Efekt: wszystkie trzy mechanizmy przechodzą sprawdzenie w Gmailu, blokada Microsoftu została zdjęta, a użytkownicy niczego nie odczuli.

W 20 minut ustalimy, jaki pierwszy krok pasuje do Państwa systemu

Państwo opisują cel i obecny stan, my mówimy otwarcie, czy i jak możemy pomóc. Odpowiedź w ciągu jednego dnia roboczego, bez zobowiązań.

Telefon (niemiecki, polski, angielski) +48 501 250 220
E-mail E-mail

Dostępni: poniedziałek–piątek, 9:00–17:00 (czas środkowoeuropejski); wsparcie i awarie całą dobę (24/7)