Tryb planowania schematu Postgres: podejście krok po kroku
Tryb planowania schematu Postgres pomaga zdefiniować encje, ograniczenia, indeksy i migracje przed generowaniem kodu, redukując konieczność późniejszych przeróbek.

Dlaczego warto zaplanować schemat Postgres przed generowaniem kodu
Jeśli budujesz endpointy i modele zanim kształt bazy będzie jasny, zwykle kończysz na dwukrotnym przepisywaniu tych samych funkcji. Aplikacja działa na demo, potem pojawiają się realne dane i edge case’y i wszystko zaczyna być kruche.
Większość przeróbek wynika z trzech przewidywalnych problemów:
- Encje nie są jasne (nazwy lub grupowania są słabe).
- Zasady nie były egzekwowane (brak ograniczeń).
- Problemy z wydajnością pojawiają się późno (indeksy dodawane dopiero po skargach użytkowników).
Każdy z tych powodów wymusza zmiany, które rozchodzą się przez kod, testy i aplikacje klienckie.
Planowanie schematu Postgres oznacza najpierw ustalenie kontraktu danych, a potem generowanie kodu do niego pasującego. W praktyce wygląda to jak zapisanie encji, relacji i kilku najważniejszych zapytań, a potem wybór ograniczeń, indeksów i podejścia do migracji zanim jakiekolwiek narzędzie stworzy tabele i CRUD.
Ma to jeszcze większe znaczenie, gdy używasz platformy vibe-codingowej jak Koder.ai, gdzie możesz szybko wygenerować dużo kodu. Szybkie generowanie jest świetne, ale jest znacznie bardziej niezawodne, gdy schemat jest ustalony. Wygenerowane modele i endpointy wymagają wtedy mniej poprawek.
Oto co zazwyczaj idzie nie tak, gdy pomijasz planowanie:
- Tabela „User” cicho zmienia się w mieszaninę użytkowników, adminów i zespołów.
- Pojawiają się duplikaty, bo nic nie wymusza unikalności.
- Usuwanie łamie historię, bo nie ustalono zachowania referencyjnego.
- Popularna strona listy zwalnia, bo nie zaplanowano właściwego indeksu.
Dobry plan schematu jest prosty: opis encji w zwykłym języku, szkic tabel i kolumn, kluczowe ograniczenia i indeksy oraz strategia migracji, która pozwala bezpiecznie zmieniać rzeczy w miarę rozwoju produktu.
Zacznij od potrzeb danych, nie od tabel
Planowanie schematu działa najlepiej, gdy zaczynasz od tego, co aplikacja musi zapamiętać i co ludzie muszą móc zrobić z tymi danymi. Napisz cel w 2–3 prostych zdaniach. Jeśli nie potrafisz tego prosto wytłumaczyć, prawdopodobnie stworzysz zbędne tabele.
Następnie skup się na akcjach, które tworzą lub zmieniają dane. Te akcje są prawdziwym źródłem wierszy i ujawniają, co trzeba walidować. Myśl czasownikami, nie rzeczownikami.
Na przykład aplikacja do rezerwacji może potrzebować tworzyć rezerwację, przełożyć ją, anulować, zwrócić pieniądze i wysłać wiadomość do klienta. Te czasowniki szybko sugerują, co trzeba przechowywać (sloty czasowe, zmiany statusu, kwoty pieniędzy), zanim w ogóle nazwiesz tabelę.
Zapisz też ścieżki odczytu, bo to one później kształtują strukturę i indeksowanie. Wymień ekrany lub raporty, z których ludzie faktycznie będą korzystać i w jaki sposób kroją dane: „Moje rezerwacje” posortowane po dacie i filtrowane po statusie, wyszukiwanie admina po nazwisku klienta lub numerze rezerwacji, dzienne przychody wg lokalizacji oraz widok audytu kto i kiedy coś zmienił.
Na koniec zanotuj potrzeby niefunkcjonalne, które wpływają na wybory schematu, jak historia audytu, soft deletes, separacja multi-tenant czy reguły prywatności (np. ograniczenie widoczności danych kontaktowych).
Jeśli planujesz generować kod po tym etapie, te notatki stają się mocnymi promptami. Wyjaśniają, co jest wymagane, co może się zmieniać i co musi być przeszukiwalne. Jeśli używasz Koder.ai, spisanie tego przed generowaniem sprawia, że Tryb Planowania jest znacznie skuteczniejszy, bo platforma działa na podstawie rzeczywistych wymagań zamiast domysłów.
Zdefiniuj encje i relacje prostym językiem
Zanim dotkniesz tabel, napisz prosty opis tego, co przechowuje aplikacja. Zacznij od wymienienia rzeczowników, które się powtarzają: user, project, message, invoice, subscription, file, comment. Każdy z nich to kandydat na encję.
Potem dodaj jedno zdanie na encję odpowiadające na pytanie: czym jest i dlaczego istnieje? Na przykład: „Project to przestrzeń robocza, którą użytkownik tworzy, by grupować pracę i zapraszać innych.” To zapobiega niejasnym tabelom jak data, items czy misc.
Własność (ownership) to następna duża decyzja i wpływa na prawie każde zapytanie, które napiszesz. Dla każdej encji zdecyduj:
- kto ją tworzy,
- kto może ją zobaczyć,
- kto może ją zmieniać,
- co się dzieje, gdy właściciel zostanie usunięty (zachować, przekazać, usunąć).
Teraz zdecyduj, jak identyfikować rekordy. UUID sprawdzają się, gdy rekordy mogą być tworzone z wielu miejsc (web, mobile, joby) lub gdy nie chcesz przewidywalnych ID. Bigint są mniejsze i szybsze. Jeśli potrzebujesz przyjaznego dla ludzi identyfikatora, trzymaj go osobno (np. krótki project_code unikalny w ramach konta) zamiast robić z niego klucz główny.
Na koniec opisuj relacje słowami zanim narysujesz diagram: user ma wiele projektów, projekt ma wiele wiadomości, a użytkownicy mogą należeć do wielu projektów. Oznacz każde powiązanie jako wymagane lub opcjonalne, np. „message musi należeć do projektu” vs „invoice może należeć do projektu.” Te zdania staną się źródłem prawdy przy generowaniu kodu.
Przekształć encje w tabele i kolumny
Gdy encje są jasne w prostym opisie, zamień każdą z nich w tabelę z kolumnami odpowiadającymi prawdziwym danym, które trzeba przechować.
Zacznij od nazw i typów, których będziesz mógł się trzymać. Wybierz spójne wzorce: nazwy kolumn w snake_case, ten sam typ dla tej samej koncepcji i przewidywalne klucze główne. Dla znaczników czasu preferuj timestamptz, żeby strefy czasowe nie robiły niespodzianek. Dla pieniędzy używaj numeric(12,2) (lub przechowuj grosze jako integer) zamiast floatów.
Dla pól statusu użyj albo enumu Postgres, albo kolumny text z CHECK, żeby dopuszczalne wartości były kontrolowane.
Zdecyduj, co jest wymagane, a co opcjonalne, tłumacząc zasady na NOT NULL. Jeśli wartość musi istnieć, by wiersz miał sens, oznacz ją jako wymaganą. Jeśli naprawdę może być nieznana lub nie dotyczy, dopuszczaj null.
Praktyczny zestaw domyślnych kolumn do zaplanowania:
id(uuid lub bigint — wybierz podejście i trzymaj się go)created_atiupdated_atdeleted_attylko jeśli naprawdę potrzebujesz soft deletes i możliwość przywróceniacreated_bygdy potrzebujesz jasnego śladu kto co zrobił
Relacje wiele-do-wielu powinny prawie zawsze stać się tabelami łączącymi. Na przykład, jeśli wielu użytkowników może współpracować nad aplikacją, utwórz app_members z app_id i user_id, a potem wymuś unikalność na parze, by duplikaty nie występowały.
Pomyśl o historii wcześniej. Jeśli wiesz, że będziesz potrzebować wersjonowania, zaplanuj niezmienną tabelę typu app_snapshots, gdzie każdy wiersz to zapisana wersja powiązana z apps przez app_id i oznaczona created_at.
Dodaj ograniczenia, które chronią dane
Ograniczenia to bariery bezpieczeństwa schematu. Zdecyduj, które zasady muszą być prawdziwe niezależnie od tego, jakie serwisy, skrypty czy narzędzia admina dotykają bazy.
Zacznij od tożsamości i relacji. Każda tabela potrzebuje klucza głównego, a każde pole „belongs to” powinno być prawdziwym kluczem obcym, nie tylko integerem, który „chyba” pasuje.
Dodaj unikalności tam, gdzie duplikaty mogą wyrządzić realne szkody, jak dwa konta z tym samym emailem lub dwie pozycje zamówienia o tym samym (order_id, product_id).
Wysokowartościowe ograniczenia do zaplanowania wcześnie:
- Primary keys: wybierz spójny styl (UUID lub bigint), żeby JOINy były przewidywalne.
- Foreign keys: jawnie określ relację i zapobiegaj osieroconym wierszom.
- Unique constraints: używaj ich dla tożsamości biznesowej (email, username) i reguł „tylko jeden z tych”.
- Check constraints: tanie reguły jak
amount >= 0,status IN ('draft','paid','canceled')lubrating BETWEEN 1 AND 5. - Not null: wymagaj pól, które w realnym życiu są obowiązkowe, nie tylko „zwykle wypełniane”.
Zachowanie kaskadowe to miejsce, gdzie planowanie oszczędza później. Zapytaj, czego ludzie naprawdę oczekują. Jeśli klient zostanie usunięty, jego zamówienia zwykle nie powinny zniknąć. To prowadzi do RESTRICT/NO ACTION i zachowywania historii. Dla zależnych danych jak pozycje zamówienia CASCADE może mieć sens, bo same pozycje nie mają znaczenia bez rodzica.
Gdy potem wygenerujesz modele i endpointy, te ograniczenia staną się jasnymi wymaganiami: jakie błędy obsługiwać, które pola są wymagane i jakie przypadki brzegowe są z założenia niemożliwe.
Planuj indeksy na podstawie rzeczywistych zapytań
Indeksy powinny odpowiadać na jedno pytanie: co musi być szybkie dla prawdziwych użytkowników.
Zacznij od ekranów i wywołań API, które planujesz wypuścić najpierw. Strona listy filtrująca po statusie i sortująca po dacie ma inne potrzeby niż strona szczegółów ładująca powiązane rekordy.
Zapisz 5–10 wzorców zapytań prostym językiem zanim wybierzesz indeks. Na przykład: „Pokaż moje faktury z ostatnich 30 dni, filtruj po opłacone/nieopłacone, sortuj po created_at” albo „Otwórz projekt i wypisz zadania po due_date”. To utrzymuje wybór indeksów przy ziemi.
Dobry pierwszy zestaw indeksów często obejmuje kolumny kluczy obcych używane w JOINach, popularne kolumny filtrów (jak status, user_id, created_at) oraz jeden lub dwa indeksy złożone dla stabilnych wielofiltrów, np. (account_id, created_at) gdy zawsze filtrujesz po account_id, a potem sortujesz po czasie.
Kolejność w indeksach złożonych ma znaczenie. Umieść kolumnę, po której filtrujesz najczęściej (i która jest najbardziej selektywna) na początku. Jeśli na każdym żądaniu filtrujesz po tenant_id, to często powinno być na początku wielu indeksów.
Unikaj indeksowania wszystkiego „na wszelki wypadek”. Każdy indeks dodaje pracę przy INSERT i UPDATE i może bardziej zaszkodzić niż lekko wolniejsze rzadkie zapytanie.
Planuj wyszukiwanie tekstowe osobno. Jeśli potrzebujesz tylko prostego dopasowania „zawiera”, ILIKE może wystarczyć na początek. Jeśli wyszukiwanie jest kluczowe, zaplanuj pełnotekstowe (tsvector) wcześnie, żeby nie przeprojektowywać później.
Zdecyduj o strategii migracji zanim wygenerujesz kod
Schemat nie jest „gotowy” po utworzeniu pierwszych tabel. Zmienia się za każdym razem, gdy dodajesz funkcję, poprawiasz błąd lub uczysz się więcej o danych. Jeśli ustalisz strategię migracji z wyprzedzeniem, unikniesz bolesnych przeróbek po wygenerowaniu kodu.
Trzymaj prostą zasadę: zmieniaj bazę małymi krokami, funkcja po funkcji. Każda migracja powinna być łatwa do przeglądu i bezpieczna do uruchomienia w każdym środowisku.
Jak bezpiecznie obsługiwać zmiany łamiące kompatybilność
Większość problemów wynika z zmiany nazwy lub usuwania kolumn albo zmiany typów. Zamiast robić wszystko jednocześnie, zaplanuj bezpieczną ścieżkę:
- Dodaj najpierw nowe kolumny (nullable jeśli trzeba), potem wypełnij dane.
- Jeśli aplikacja musi pozostać dostępna, użyj dual-write przez krótki czas (zapisuj do starego i nowego pola).
- Przełącz odczyty na nowe pole, potem posprzątaj stare kolumny w kolejnej migracji.
To wymaga więcej kroków, ale w praktyce jest szybsze, bo ogranicza przerwy i awaryjne poprawki.
Seed danych też jest częścią migracji. Zdecyduj, które tabele referencyjne są „zawsze” (role, statusy, kraje, typy planów) i uczyn je przewidywalnymi. Wstawienia i aktualizacje dla tych tabel umieść w dedykowanych migracjach, żeby każdy developer i każde wdrożenie miało te same wyniki.
Zasady spójności między lokalnym, dev i produkcją
Ustal oczekiwania wcześnie:
- Te same migracje, w tej samej kolejności, wszędzie.
- Żadnych ręcznych hotfixów w produkcji bez odpowiadającej migracji.
- Każda migracja ma jasną ścieżkę forward i realistyczny plan rollback.
Rollbacky nie zawsze są idealnymi „down” migracjami. Czasami najlepszy rollback to przywrócenie z backupu. Jeśli używasz Koder.ai, warto też zdecydować, kiedy polegać na snapshotach i rollbacku dla szybkiego odzyskania, szczególnie przed ryzykownymi zmianami.
Prosty przykład, który możesz skopiować i dostosować
Wyobraź sobie małą aplikację SaaS, gdzie ludzie dołączają do zespołów, tworzą projekty i śledzą zadania.
Zacznij od wypisania encji i tylko pól potrzebnych na dzień pierwszy:
- users: id, email, full_name, created_at
- teams: id, org_id, name, created_at
- team_members: team_id, user_id, role, joined_at
- projects: id, team_id, name, status, created_at
- tasks: id, project_id, assignee_user_id (nullable), title, state, due_date (nullable), created_at
Relacje są proste: team ma wiele projektów, projekt ma wiele zadań, a użytkownicy dołączają do teamów przez team_members. Zadania należą do projektu i mogą być przypisane do użytkownika.
Dodaj kilka ograniczeń, które zapobiegają typowym błędom wykrywanym zbyt późno:
- Zrób users.email unikalnym (case-insensitive jeśli używasz
citext). - Zrób nazwy teamów unikalnymi w ramach tej samej organizacji: UNIQUE (org_id, name) na teams.
- Zapobiegaj duplikatom członkostwa: UNIQUE (team_id, user_id) na team_members.
Indeksy powinny odpowiadać realnym ekranom. Na przykład, jeśli lista zadań filtruje po projekcie i stanie oraz sortuje po newest, zaplanuj indeks tasks (project_id, state, created_at DESC). Jeśli „Moje zadania” to kluczowy widok, indeks tasks (assignee_user_id, state, due_date) może pomóc.
Dla migracji, trzymaj pierwszą serię bezpieczną i nudną: twórz tabele, klucze główne, klucze obce i podstawowe ograniczenia unikalności. Dobrym późniejszym dodatkiem jest wprowadzenie soft delete (deleted_at) na tasks i dostosowanie indeksów „active tasks”, by ignorowały usunięte wiersze.
Typowe błędy i jak ich unikać
Większość przeróbek wynika z tego, że pierwszy schemat nie zawiera reguł i szczegółów użycia. Dobre planowanie nie polega na idealnych diagramach. Chodzi o wczesne wykrycie pułapek.
Częsty błąd to trzymanie ważnych reguł wyłącznie w kodzie aplikacji. Jeśli wartość musi być unikalna, obecna lub mieścić się w zakresie, baza danych też powinna to egzekwować. W przeciwnym razie job w tle, nowy endpoint lub ręczny import może obejść logikę.
Inne częste przeoczenie to traktowanie indeksów jako problemu później. Dodawanie ich po starcie często zamienia się w zgadywanie i możesz indeksować niewłaściwe kolumny, podczas gdy prawdziwy wolny zapytanie dotyczy JOIna lub filtra po statusie.
Tabele wiele-do-wielu też są źródłem cichych bugów. Jeśli tabela łącząca nie zapobiega duplikatom, możesz zapisać to samo powiązanie dwukrotnie i spędzić godziny na debugowaniu „dlaczego ten użytkownik ma dwie role?”.
Łatwo też stworzyć tabele, a potem zorientować się, że potrzebne są logi audytu, soft deletes lub historia zdarzeń. Te dodatki rozlewają się na endpointy i raporty.
Wreszcie, kolumny JSON kuszą elastycznością, ale usuwają walidację i utrudniają indeksowanie. JSON jest w porządku dla naprawdę zmiennego payloadu, nie dla kluczowych pól biznesowych.
Zanim wygenerujesz kod, zrób szybką listę kontrolną:
- Przenieś kluczowe zasady do ograniczeń:
NOT NULL,CHECK,UNIQUEi klucze obce. - Napisz 3–5 prawdziwych zapytań i zaplanuj indeksy pod nie, nie pod „może później”.
- Dodaj złożone UNIQUE na tabelach łączących (np.
user_id + role_id). - Zdecyduj wcześnie, czy potrzebujesz historii audytu i zamodeluj ją jako odrębną tabelę.
- Trzymaj JSON dla rzadkich, opcjonalnych atrybutów; promuj ważne pola do kolumn.
Krótka checklista przed generowaniem modeli i endpointów
Zatrzymaj się i upewnij, że plan jest wystarczająco kompletny, by wygenerować kod bez gonienia niespodzianek. Cel nie to perfekcja, lecz wykrycie luk, które powodują późniejsze przeróbki: brakujące relacje, niejasne zasady i indeksy niepasujące do rzeczywistego użycia.
Użyj tego jako szybkiego pre-flight check:
- Dla każdej encji napisz jedno zdanie kto ją posiada (user, org, system) i co znaczy „usunąć” (hard delete, soft delete, archived).
- Dla każdej tabeli potwierdź typ kolumny, wymagane vs opcjonalne, wartości domyślne i sposób ustawiania znaczników czasu (aplikacja czy baza).
- Zapisz zasady, które nigdy nie mogą być złamane: klucze główne, klucze obce, reguły unikalności i kilka prostych checków (jak
amount >= 0lub dozwolone statusy). - Wybierz swoje top 3–5 ekranów lub wywołań API i wypisz dokładne filtry oraz kolejność sortowania, jakich potrzebują. Upewnij się, że indeksy pasują.
- Naszkicuj pierwsze migracje: początkowy schemat, dane, które muszą istnieć w dniu pierwszym (seed rows) i jedną zmianę, której spodziewasz się wkrótce (dodanie statusu, rozdzielenie pola imienia i nazwiska, wprowadzenie orgów).
Szybki test sanity: udawaj, że jutro dołącza kolega. Czy mógłby zbudować pierwsze endpointy bez pytania co godzinę „czy to może być null?” lub „co się dzieje przy usunięciu?”?
Następne kroki: przejdź od planu do kodu z mniejszą liczbą przeróbek
Gdy plan jest czytelny i główne przepływy sensowne na papierze, zamień go w coś wykonalnego: realny schemat i migracje.
Zacznij od początkowej migracji tworzącej tabele, typy (jeśli używasz enumów) i obowiązkowe ograniczenia. Utrzymaj pierwszy przebieg mały, ale poprawny. Załaduj trochę danych seed i odpal zapytania, których aplikacja będzie naprawdę potrzebować. Jeśli jakiś przepływ wydaje się niezręczny, popraw schemat, gdy historia migracji jest jeszcze krótka.
Generuj modele i endpointy dopiero po tym, jak przetestujesz kilka end-to-end akcji na schemacie (create, update, list, delete oraz jedna prawdziwa akcja biznesowa). Generowanie kodu jest najszybsze, gdy tabele, klucze i nazewnictwo są wystarczająco stabilne, żeby nie zmieniać wszystkiego następnego dnia.
Praktyczna pętla, która utrzymuje niską liczbę przeróbek:
- Aktualizuj plan, gdy nauczysz się czegoś nowego w testach.
- Dodaj nową migrację (unikaj edytowania starych, gdy inni już ich używają).
- Regeneruj modele i endpointy, jeśli schemat się zmienił.
- Uruchom ponownie te same przepływy i zapytania, by potwierdzić, że nic nie zepsuto.
- Dodawaj pola „miłe do posiadania” i dodatkowe indeksy dopiero po ustabilizowaniu głównych ścieżek.
Zdecyduj wcześnie, co weryfikujesz w bazie, a co w warstwie API. Umieść stałe reguły w bazie (klucze obce, unikalność, check constraints). Trzymaj reguły miękkie w API (feature flagi, tymczasowe limity i złożona logika między tabelami, która często się zmienia).
Jeśli używasz Koder.ai, rozsądne podejście to uzgodnić encje i migracje w Trybie Planowania najpierw, a potem wygenerować backend w Go + PostgreSQL. Gdy zmiana pójdzie nie tak, snapshoty i rollback pomogą szybko wrócić do znanej dobrej wersji, podczas gdy dopracowujesz plan schematu.
Często zadawane pytania
Why should I plan my Postgres schema before generating models and endpoints?
Zaplanuj schemat najpierw. Ustala to stabilny kontrakt danych (tabele, klucze, ograniczenia), dzięki czemu wygenerowane modele i endpointy nie będą wymagać ciągłych zmian i renamów.
W praktyce: zapisz encje, relacje i najważniejsze zapytania, a potem zablokuj ograniczenia, indeksy i strategię migracji przed wygenerowaniem kodu.
What’s the fastest way to start schema planning without getting stuck on diagrams?
Napisz 2–3 zdania opisujące, co aplikacja musi zapamiętać i co użytkownicy muszą móc z tym zrobić.
Następnie wypisz:
- akcje, które tworzą/zmieniają dane (czasowniki)
- ekrany/raporty, z których ludzie będą korzystać (ścieżki odczytu)
- potrzeby niefunkcjonalne jak historia audytu, soft delete, multi-tenancy i prywatność
To daje wystarczająco jasności, by projektować tabele bez nadmiaru elementów.
How do I know what my core entities are?
Zacznij od listy rzeczowników, które się powtarzają (user, project, invoice, task). Dla każdego dopisz jedno zdanie: czym jest i po co istnieje.
Jeśli nie potrafisz tego jasno opisać, prawdopodobnie skończysz z niejasnymi tabelami typu items czy misc i późniejszym żalem.
Should I use UUIDs or bigint IDs in Postgres?
Trzymaj się jednej spójnej strategii identyfikatorów w całym schemacie.
- UUID: świetne, gdy rekordy mogą powstawać z wielu miejsc (web, mobile, joby) lub nie chcesz przewidywalnych ID
- bigint: mniejsze, nieco szybsze i prostsze, gdy wszystko tworzone jest po stronie serwera
Jeśli potrzebujesz czytelnego identyfikatora dla ludzi, dodaj osobną unikalną kolumnę (np. project_code) zamiast używać jej jako klucza głównego.
How do I choose the right delete behavior for foreign keys (CASCADE vs RESTRICT)?
Decyduj dla każdej relacji, biorąc pod uwagę oczekiwania użytkowników i konieczność zachowania danych.
Typowe domyślne podejścia:
- Zachowaj historię: użyj
RESTRICT/NO ACTION, gdy usunięcie rodzica nie powinno usuwać ważnych rekordów (np. customers → orders) - Bezpieczne kaskadowanie: użyj
CASCADE, gdy dzieci nie mają sensu bez rodzica (np. order → line items)
Podejmij tę decyzję wcześnie, bo wpływa na zachowanie API i przypadki brzegowe.
Which constraints should I add on day one?
Wprowadź stałe reguły w bazie, żeby każdy klient (API, skrypty, importy, narzędzia admina) był do nich zmuszony.
Priorytety na dzień dobry:
PRIMARY KEYna każdej tabeliFOREIGN KEYdla każdego pola „belongs to”UNIQUEtam, gdzie duplikaty powodują realne szkody (email,(team_id, user_id)w tabelach łączących)CHECKdla prostych zasad (nieujemne kwoty, dozwolone statusy)NOT NULLdla pól niezbędnych do sensu wiersza
How do I pick indexes without over-indexing?
Zacznij od rzeczywistych wzorców zapytań, nie od przypuszczeń.
Wypisz 5–10 zapytań w prostym języku (filtry + sort), potem indeksuj pod te przypadki:
- kolumny kluczy obcych używane w JOINach
- popularne filtry jak
status,user_id,created_at - kilka indeksów złożonych odpowiadających stabilnym wzorcom (np.
(account_id, created_at))
Unikaj indeksowania wszystkiego; każdy indeks spowalnia INSERT i UPDATE.
What’s the right way to model many-to-many relationships?
Utwórz tabelę łączącą z dwoma kluczami obcymi i złożonym ograniczeniem unikalności.
Przykład:
team_members(team_id, user_id, role, joined_at)- Dodaj
UNIQUE (team_id, user_id), by zapobiec duplikatom
To eliminuje subtelne błędy typu „dlaczego ten użytkownik występuje dwa razy?” i upraszcza zapytania.
What types and column patterns are good defaults in Postgres?
Domyślnie:
timestamptzdla znaczników czasu (mniej zaskoczeń związanych ze strefami czasowymi)numeric(12,2)lub całkowite grosze dla pieniędzy (unikaj floatów)- wymuszane wartości statusów przez enumy Postgres lub
CHECK
Utrzymuj spójność typów między tabelami (ta sama koncepcja = ten sam typ), aby JOINy i walidacje były przewidywalne.
What migration strategy helps avoid breaking generated code later?
Używaj małych, przeglądalnych migracji i unikaj jednorazowych, łamiących zmian.
Bezpieczna ścieżka:
- Dodaj najpierw nowe kolumny (nullable, jeśli trzeba)
- Wypełnij dane (backfill)
- Krótko dual-write, jeśli aplikacja musi pozostać dostępna (zapis do starego i nowego pola)
- Przełącz odczyty na nowe pole
- Usuń stare kolumny później
Zdecyduj też wcześniej, jak obsługujesz dane referencyjne/seed, żeby środowiska były spójne.