SQL to kontrakt, który przetrwa twój ORM

2026-09-10

Każdy produkt, który wdrożyłem, przetrwał co najmniej jedną decyzję o frameworku. Baza danych przetrwała je wszystkie. Zmieniają się wersje Next.js, ORM-y i skład zespołu; wiersze pozostają, podobnie jak założenia zapisane w schemacie. Dlatego SQL traktuję jako prawdziwy kontrakt produktu, a nie szczegół implementacji ukryty za klasą repository.

Prisma obsługuje dużą część mojej codziennej pracy: typowane CRUD, migracje w repozytorium i schemat czytelny dla nowej osoby. Czytam jednak także generowany przez nią SQL. Jeśli strona listy uruchamia zapytanie dla każdego wiersza, raport wymaga funkcji okna albo dashboard potrzebuje GROUP BY na trzech joinach, piszę zapytanie ręcznie przez $queryRaw lub umieszczam je w widoku. Zanim ktoś doda Redis, uruchamiam EXPLAIN ANALYZE. Za większością zgłoszeń „potrzebujemy cache’a” kryje się brak indeksu.

Ograniczenia to najtańsze QA, jakie można sobie zapewnić. NOT NULL, UNIQUE, FOREIGN KEY i CHECK działają przy każdym zapisie z każdej usługi, także ze skryptu uruchomionego ręcznie w piątek. Zod chroni API, baza danych chroni prawdę. Pieniądze zapisujemy jako całkowite jednostki mniejsze albo NUMERIC — nigdy float. Webhooki płatnicze przychodzą dwukrotnie, dlatego klucz idempotencji powinien mieć unikalny indeks, a nie komentarz w handlerze.

Błędy FinTech rodzą się przy współbieżności. Dwa żądania odczytują saldo, oba uznają je za wystarczające i oba zapisują zmianę — to utracona aktualizacja, której nie naprawi żadna walidacja frontendowa. Wolę jedno atomowe polecenie —UPDATE ... SET balance = balance - $1 WHERE id = $2 AND balance >= $1— a następnie sprawdzam, czy zmienił się dokładnie jeden wiersz. Alternatywą jest transakcja z SELECT ... FOR UPDATE i tabelą ledger w trybie append-only, by później dało się wyjaśnić każdy ruch. W systemach z obrotem przekraczającym 1,5 mln dolarów możliwość „późniejszego wyjaśnienia” stanowi sedno pracy.

Indeksy powinny odpowiadać zapytaniom, które faktycznie wykonujesz, a nie ważnie wyglądającym kolumnom. W indeksie złożonym kolumny z warunkiem równości są pierwsze, a zakresu lub sortowania — ostatnie; indeksy częściowe obsługują wiersze „aktywne”, a paginacja keyset jest lepsza niż OFFSET 50000. Migracje na działającej tabeli przeprowadza się etapami: dodać kolumnę jako nullable, uzupełnić partiami, potem dodać ograniczenie; indeks zbudować przez CONCURRENTLY. Migracja blokująca tabelę zamówień w południe to awaria z dołączonym pull requestem.

Odrzucam „elastyczną” tabelę entity-attribute-value zamiast schematu, JSONB jako skrytkę na kluczowe pola, których nikt nie chce modelować, soft delete wszędzie bez uwzględnienia ograniczeń unikalności oraz cache przed zapytaniem, którego nikt nie sprawdził przez EXPLAIN. Jeśli wyślesz mi wolną stronę lub niepewny schemat, dołącz zapytanie i liczbę wierszy. Odpowiem planem, indeksem i pierwszym ograniczeniem, które bym dodał.

Daj kolejnemu produktowi kogoś, kto zostanie.

Chcesz zacząć? Napisz do mnie — mogę rozpocząć od razu i zostać na dłużej. Długoterminowa współpraca, nie przekazanie projektu po dwóch tygodniach. Omówmy projekt, wymagania i najlepszy sposób realizacji.

Zapytaj o doświadczenie lub usługi