Menu

Indeksy złożone w SQLite: kolejność kolumn i reguła lewego prefiksu

Jak działają indeksy wielokolumnowe w SQLite, dlaczego kolejność kolumn ma znaczenie i kiedy indeks złożony pomaga, a kiedy tylko marnuje miejsce.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Jeden indeks, kilka kolumn

Indeks złożony, nazywany też indeksem wielokolumnowym, to jeden indeks zbudowany na dwóch lub więcej kolumnach. Tworzysz go, wymieniając kolumny w kolejności:

Indeks idx_orders_customer_status przechowuje wpisy posortowane najpierw według customer_id, a potem według status w obrębie każdego klienta. Ta kolejność to cała historia: wszystko inne w indeksach złożonych z niej wynika.

Model myślowy: posortowana książka telefoniczna

Wyobraź sobie starą książkę telefoniczną. Wpisy są posortowane według nazwiska, a w obrębie nazwiska według imienia. Tak dokładnie wygląda indeks na (last_name, first_name).

Niektóre wyszukiwania są tanie, inne nie:

  • "Znajdź wszystkich o nazwisku Patel": łatwe, wszyscy Patelowie stoją obok siebie.
  • "Znajdź Priyę Patel": łatwe, skocz do Patel, a potem przewiń do Priyi.
  • "Znajdź wszystkie osoby o imieniu Priya": wolne, trzeba przejrzeć każdą stronę. Priye są rozrzucone po wszystkich nazwiskach.

Indeks złożony w SQLite działa tak samo. Pierwsza kolumna to główny klucz sortowania, a druga sortuje tylko wpisy o tej samej wartości pierwszej kolumny.

Reguła lewego prefiksu

SQLite może użyć indeksu złożonego tylko wtedy, gdy klauzula WHERE ogranicza lewy prefiks jego kolumn. Dla indeksu na (a, b, c):

  • Filtrowanie po a: indeks jest używany.
  • Filtrowanie po a i b: indeks jest używany.
  • Filtrowanie po a, b i c: indeks jest używany.
  • Filtrowanie tylko po b, tylko po c albo po b i c: indeks nie jest używany.

Możesz to sprawdzić bezpośrednio przez EXPLAIN QUERY PLAN:

Pierwszy plan pokazuje SEARCH events USING INDEX idx_events_user_kind_time. Drugi wraca do SCAN events: filtrowanie tylko po kind pomija pierwszą kolumnę user_id, więc indeks jest dla tego zapytania bezużyteczny.

Kolejność kolumn to decyzja projektowa

Skoro lewy prefiks ma znaczenie, kolejność kolumn w CREATE INDEX to prawdziwy wybór, a nie kwestia stylu. Dwie praktyczne zasady:

  1. Na początku umieść kolumnę, po której filtrujesz najczęściej. Ta kolumna otwiera indeks dla najszerszego zakresu zapytań.
  2. Kolumny z równością umieszczaj przed kolumnami z zakresem. SQLite potrafi zejść w głąb indeksu przez =, a potem przeskanować ciągły zakres przez <, > lub BETWEEN, ale tylko na ostatniej użytej kolumnie.

Plan pokazuje SEARCH sales USING INDEX idx_sales_region_time (region=? AND sold_at>?). SQLite skacze prosto do region = 'EU', a potem idzie naprzód przez zakres dat. Odwróć kolejność kolumn na (sold_at, region), a to samo zapytanie będzie musiało przeskanować każdy wiersz z zakresu dat i dla każdego ponownie sprawdzić region.

Indeks złożony a kilka indeksów jednokolumnowych

Częste pytanie: czy utworzyć jeden indeks na (a, b), czy dwa osobne indeksy na a i b?

Przy łącznym filtrze indeks złożony jest szybszy: SQLite idzie prosto do pasujących wpisów (project_id, state). Przy dwóch indeksach jednokolumnowych SQLite zwykle wybiera jeden, używa go do zawężenia wierszy, a potem sprawdza drugą kolumnę w każdym pasującym wierszu. Czasem potrafi je przeciąć, ale indeks złożony to czystsze rozwiązanie, gdy kolumny są odpytywane razem.

Jeśli project_id i state są też odpytywane niezależnie, możesz chcieć obu: indeksu złożonego dla łącznego filtra i indeksu jednokolumnowego na state dla zapytań, które filtrują tylko po nim.

Indeksy pokrywające

Gdy indeks zawiera każdą kolumnę potrzebną zapytaniu, zarówno kolumny filtra, jak i wybierane kolumny, SQLite może odpowiedzieć na zapytanie w ogóle bez dotykania tabeli. To indeks pokrywający (covering index) i szybciej zapytanie już nie zadziała.

Plan pokazuje USING COVERING INDEX idx_invoices_cover. Zapytanie czyta issued_at i total prosto z indeksu: notes i id nie są potrzebne, więc sama tabela nigdy nie jest otwierana. Dodanie kolumny do indeksu złożonego wyłącznie po to, żeby pokrył często wykonywane zapytanie, to opłacalna wymiana, gdy to zapytanie działa bez przerwy.

Złożone ograniczenia UNIQUE

Indeksy złożone wymuszają też unikalność kombinacji kolumn. Przydaje się to, gdy żadna kolumna sama w sobie nie jest unikalna, ale ich kombinacja musi być:

Trzecie wstawienie zgłasza UNIQUE constraint failed: enrollments.student_id, enrollments.course_id. Ta sama para już istnieje w indeksie, więc SQLite odrzuca duplikat.

Pułapki, które warto znać

  • OR między kolumnami innymi niż pierwsza blokuje indeks. WHERE a = 1 OR b = 2 przy indeksie (a, b) zwykle w ogóle nie może użyć indeksu, bo SQLite musi rozważyć obie gałęzie osobno.
  • Funkcje na indeksowanych kolumnach wyłączają indeks. WHERE lower(email) = 'x' nie użyje indeksu na email. Zamiast tego zindeksuj wyrażenie albo normalizuj dane przy wstawianiu.
  • Indeksy nie są za darmo. Każdy indeks jest aktualizowany przy każdym INSERT, UPDATE (indeksowanych kolumn) i DELETE. Trzy indeksy złożone na tabeli z dużą liczbą zapisów mogą zdominować koszt zapisu.
  • Po zbudowaniu indeksów uruchom ANALYZE. Planer SQLite używa statystyk zebranych przez ANALYZE, aby wybierać między kandydującymi indeksami. Bez tych statystyk korzysta z heurystyk, które nie zawsze są optymalne.

Praktyczny sposób pracy

Przy strojeniu wolnego zapytania pętla zwykle wygląda tak:

  1. Uruchom EXPLAIN QUERY PLAN dla zapytania, aby zobaczyć, co SQLite robi dziś.
  2. Jeśli skanuje, spójrz na klauzulę WHERE: która kolumna jest porównywana przez równość? Która przez zakres? Co jest wybierane?
  3. Zbuduj indeks złożony z kolumnami w kolejności: najpierw równość, potem zakres, a na końcu wybierane kolumny, jeśli pokrycie pomaga.
  4. Uruchom ANALYZE.
  5. Ponownie uruchom EXPLAIN QUERY PLAN. Potwierdź, że plan się zmienił i indeks jest używany.
  6. Zmierz czas zapytania przed zmianą i po niej na reprezentatywnych danych.

Pomijasz krok 6 na własne ryzyko. Indeks, który w planie wygląda poprawnie, w praktyce wciąż może być wolniejszy, jeśli tabela jest mała albo planer wybierze inną ścieżkę.

Dalej: indeksy częściowe

Indeksy złożone obejmują każdy wiersz tabeli. Często jednak liczy się tylko mały podzbiór wierszy: otwarte zgłoszenia, nieprzetworzone zadania, nieusunięte rekordy. Indeks częściowy pozwala zindeksować tylko te wiersze dzięki klauzuli WHERE wbudowanej w sam indeks. O tym na następnej stronie.

Najczęściej zadawane pytania

Czym jest indeks złożony w SQLite?

Indeks złożony to jeden indeks obejmujący dwie lub więcej kolumn. Tworzysz go przez CREATE INDEX idx_name ON table(col_a, col_b). SQLite przechowuje wpisy posortowane najpierw według col_a, a potem według col_b w obrębie każdej wartości col_a, jak książka telefoniczna posortowana według nazwiska, a potem imienia.

Czy kolejność kolumn w indeksie złożonym SQLite ma znaczenie?

Tak, i to duże. SQLite może użyć indeksu złożonego tylko wtedy, gdy klauzula WHERE filtruje po lewym prefiksie indeksowanych kolumn. Indeks na (a, b, c) pomaga zapytaniom filtrującym po a, po a i b albo po wszystkich trzech, ale nie pomoże zapytaniu, które filtruje tylko po b lub tylko po c.

Kiedy użyć indeksu złożonego zamiast osobnych indeksów jednokolumnowych?

Używaj indeksu złożonego, gdy zapytania regularnie filtrują lub sortują po tej samej kombinacji kolumn naraz. Osobne indeksy jednokolumnowe sprawdzają się, gdy każdą kolumnę odpytuje się niezależnie. Uruchom EXPLAIN QUERY PLAN, aby zobaczyć, który indeks SQLite naprawdę wybiera: to jedyna wiarygodna informacja zwrotna.

Czym jest indeks pokrywający w SQLite?

Indeks pokrywający zawiera każdą kolumnę potrzebną zapytaniu, więc SQLite może odpowiedzieć prosto z indeksu, bez dotykania tabeli. EXPLAIN QUERY PLAN pokazuje wtedy USING COVERING INDEX. Dodawanie kolumn do indeksu złożonego tylko po to, żeby pokrył często wykonywane zapytanie, to popularna optymalizacja.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ