Przejdź do głównej zawartości

Wzorce optymalizacji SQL

Zapytanie do dashboardu, które na Twoim laptopie wykonywało się w 40 ms, w produkcji przekracza limit czasu po 12 sekundach. Tabela urosła z 50 tys. wierszy do 80 mln, optymalizator po cichu przełączył się na skanowanie sekwencyjne, a Twój APM świeci się na czerwono. Wklejasz zapytanie do asystenta AI i dostajesz pewną siebie odpowiedź „dodaj indeks na customer_id” — tyle że ten indeks już istnieje, a planner i tak go ignoruje.

Rozwiązaniem jest przestać zgadywać. Kiedy podłączysz serwer MCP bazy danych do Cursora, Claude Code lub Codeksa, asystent czyta Twój rzeczywisty plan wykonania, Twoje rzeczywiste statystyki tabel i Twoje rzeczywiste użycie indeksów — a potem proponuje zmiany, które możesz zweryfikować na prawdziwych liczbach zamiast na ludowych wierzeniach.

  • Podłączony serwer MCP Postgresa, dzięki któremu asystent czyta żywy schemat i wynik EXPLAIN zamiast halucynować
  • Gotowy prompt, który zamienia zrzut EXPLAIN (ANALYZE, BUFFERS) w uporządkowaną listę wąskich gardeł
  • Prompt rekomendujący indeksy na podstawie rzeczywistych wzorców zapytań — i odrzucający te zbędne
  • Prompt przepisujący podzapytanie na funkcję okna z zachowaniem wyników
  • Jasne wyczucie, kiedy rady AI wprowadzą Cię w błąd (małe tabele, zmaterializowane CTE, efekty uboczne EXPLAIN ANALYZE)

Bez serwera MCP asystent rozumuje o schemacie, którego nie widzi. Z nim może uruchomić EXPLAIN, zajrzeć do pg_stat_user_indexes i sprawdzić statystyki kolumn bezpośrednio. Konfiguracja jest identyczna w Cursorze, Claude Code i Codeksie — wszystkie czytają tę samą konfigurację MCP; różni się tylko lokalizacja pliku.

Do pracy lokalnej i operacji świadomych schematu Prisma Postgres MCP jest dostarczany w ramach Prisma CLI i nie wymaga dodatkowej instalacji:

{
"mcpServers": {
"postgres": {
"command": "npx",
"args": ["-y", "prisma", "mcp"]
}
}
}

Wrzuć konfigurację do .cursor/mcp.json (Cursor), zarejestruj ją przez claude mcp add (Claude Code) lub dodaj do ~/.codex/config.toml w sekcji [mcp_servers.postgres] (Codex). Potem opisany niżej przepływ pracy jest wszędzie taki sam.

Pojedynczym ruchem o największej dźwigni jest podanie asystentowi prawdziwego planu EXPLAIN (ANALYZE, BUFFERS) i poproszenie go o uszeregowanie problemów według kosztu. Ogólne prompty „zoptymalizuj to zapytanie” dają ogólne porady; prompt oparty na planie daje konkrety.

To właśnie więzy oparte na danych — „sprawdź pg_stats”, „nie sugeruj istniejących indeksów”, „oznacz Rows Removed by Filter” — odróżniają tę odpowiedź od tego, co zwróciłaby wyszukiwarka. Rozjazd szacunku liczby wierszy o 10x niemal zawsze oznacza nieaktualne statystyki (ANALYZE tabeli), a nie brakujący indeks — i asystent wychwyci to tylko wtedy, gdy każesz mu spojrzeć.

Gdy plan wskaże na skanowanie, kolejne pytanie brzmi: który indeks — i czy już nie masz takiego, który wykonuje to zadanie. Tu czytanie pg_stat_user_indexes bije intuicję: zespoły rutynowo dźwigają po kilkanaście nieużywanych indeksów, które spowalniają każdy zapis.

W trybie Agent, z podłączonym Postgres MCP, dołącz schemat i folder migracji jako kontekst i pozwól agentowi zarówno zdiagnozować, jak i napisać migrację:

@migrations Using the Postgres MCP, query pg_stat_user_indexes and
pg_stat_user_tables for the orders and order_items tables. Then:
- List indexes with idx_scan = 0 over the last stats window (candidates to drop)
- Propose the minimal set of indexes for the WHERE/JOIN/ORDER BY in
the slow dashboard query, preferring one composite index over several
single-column ones where the leading column is selective
- Write the CREATE INDEX CONCURRENTLY statements as a new migration file
- Estimate the write-amplification cost of each new index

Wzorce przepisywania, z których optymalizator Cię nie wybawi

Dział zatytułowany „Wzorce przepisywania, z których optymalizator Cię nie wybawi”

Część wolnych zapytań to nie problem indeksowania — to problem kształtu. Skorelowane podzapytania, które uruchamiają się ponownie dla każdego wiersza, to klasyczny przypadek, a lekarstwem jest funkcja okna. Zawsze proś asystenta, by udowodnił, że przepisana wersja zwraca identyczne wiersze.

Asystent powinien zwrócić oczywistą postać SUM(...) OVER (PARTITION BY customer_id ORDER BY order_date) — ale to weryfikacja EXCEPT w obie strony jest tym, co naprawdę się liczy. Domyślne ramki okna różnią się od naiwnego podzapytania (RANGE kontra ROWS, obsługa remisów przy równym order_date), a po cichu zmienione wyniki to najdroższy rodzaj „optymalizacji”.

Konkretnie dla PostgreSQL 18 dopominaj się o właściwy typ indeksu pod dany wzorzec dostępu, zamiast wszędzie wstawiać B-tree:

-- Indeks częściowy dla gorącej ścieżki (tylko aktywne wiersze) — mniejszy, szybszy
CREATE INDEX CONCURRENTLY idx_active_customers
ON customers (customer_id) WHERE status = 'active';
-- GIN dla zawierania JSONB / wyszukiwania pełnotekstowego
CREATE INDEX CONCURRENTLY idx_product_attributes
ON products USING gin (attributes);
-- BRIN dla danych naturalnie uporządkowanych w czasie, tylko-do-dopisywania — maleńki na dysku
CREATE INDEX CONCURRENTLY idx_events_created_at
ON events USING brin (created_at);

Strategiczna denormalizacja dla ścieżek ciężkich odczytowo

Dział zatytułowany „Strategiczna denormalizacja dla ścieżek ciężkich odczytowo”

Gdy złączenie jest gorące, a leżące pod nim wiersze rzadko się zmieniają, utrzymywana tabela podsumowań bije ponowne agregowanie przy każdym żądaniu. Poproś asystenta o wygenerowanie zarówno tabeli, jak i wyzwalacza, który utrzymuje ją w poprawnym stanie — to o wyzwalaczu ludzie zapominają, a nieaktualne podsumowanie jest gorsze niż wolne zapytanie.

Subtelna pułapka dokładności: APPROX_COUNT_DISTINCT() nie jest natywną funkcją PostgreSQL ani MySQL. Istnieje w BigQuery, Snowflake i Sparku, więc sugestie AI, które ją zawierają, na czystym Postgresie rzucą błędem function approx_count_distinct does not exist.

  • BigQuery / Snowflake: APPROX_COUNT_DISTINCT(customer_id) jest natywne — używaj śmiało.
  • PostgreSQL: zainstaluj rozszerzenie hll i używaj hll_cardinality(hll_add_agg(hll_hash_bigint(customer_id))) albo pogódź się z dokładnym COUNT(DISTINCT ...) wspartym indeksem pokrywającym.
  • MySQL: nie ma wbudowanego przybliżonego liczenia odrębnych wartości; użyj dokładnego COUNT(DISTINCT ...) lub utrzymuj tabelę licznikową.

To dokładnie ten rodzaj błędu między silnikami, któremu zapobiega podłączony serwer MCP — asystent może potwierdzić, że funkcja istnieje, zanim ją zaproponuje.