UUID kontra bigint w PostgreSQL: wybór ID, które się skalują
UUID kontra bigint w PostgreSQL: porównanie rozmiaru indeksów, porządku sortowania, gotowości do shardingu oraz jak ID przepływają przez API, web i aplikacje mobilne.

Dlaczego wybór ID ma większe znaczenie, niż się wydaje
Każdy wiersz w tabeli PostgreSQL potrzebuje stabilnego sposobu, by go ponownie odnaleźć. ID robi właśnie to: jednoznacznie identyfikuje rekord, zwykle jest kluczem głównym i staje się spoiwem relacji. Inne tabele przechowują go jako klucz obcy, zapytania łączą się po nim, a aplikacje przekazują go jako uchwyt dla „tego klienta”, „tej faktury” czy „tego zgłoszenia”.
Ponieważ ID pojawia się wszędzie, wybór to nie tylko szczegół bazy danych. Objawia się później w rozmiarze indeksów, wzorcach zapisu, szybkości zapytań, trafieniach w cache, a także w pracach produktowych takich jak analityka, importy czy debugowanie. Wpływa też na to, co ujawniasz w URL-ach i API, oraz jak łatwo aplikacja mobilna przechowuje i synchronizuje dane.
Większość zespołów porównuje UUID vs bigint w PostgreSQL. Mówiąc prosto, wybierasz między:
- bigint: 64-bitowa liczba, często generowana przez sekwencję (1, 2, 3...).
- UUID: 128-bitowy identyfikator, zwykle wyglądający jak losowy ciąg, albo generowany w sposób uwzględniający czas.
Żadna opcja nie wygrywa we wszystkim. Bigint jest kompaktowy i przyjazny dla indeksów oraz sortowania. UUIDy dobrze pasują, gdy potrzebujesz globalnej unikalności między systemami, chcesz bezpieczniejszych publicznych ID lub spodziewasz się tworzenia danych w wielu miejscach (wiele usług, aplikacje offline czy przyszły sharding).
Przydatna zasada: decyduj na podstawie tego, jak dane będą tworzone i udostępniane, a nie tylko jak będą przechowywane dziś.
Podstawy bigint i UUID prostym językiem
Gdy ludzie porównują UUID vs bigint w PostgreSQL, wybierają między dwoma sposobami na nazywanie wierszy: małym, licznikopodobnym numerem albo dłuższym, globalnie unikalnym identyfikatorem.
ID typu bigint to 64-bitowa liczba. W PostgreSQL zwykle generuje się ją przez kolumnę identity (lub starszy wzorzec serial). Baza trzyma sekwencję i przydziela następny numer przy każdym wstawieniu. Oznacza to, że ID zwykle rosną: 1, 2, 3, 4... Są proste, czytelne i wygodne w narzędziach oraz raportach.
UUID (Universally Unique Identifier) ma 128 bitów. Często zobaczysz go jako 36 znaków z myślnikami, np. 550e8400-e29b-41d4-a716-446655440000. Popularne typy to:
- v4: losowe UUID. Proste do wygenerowania wszędzie, ale nie sortują się po czasie utworzenia.
- v7: UUID porządkujące się według czasu. Nadal unikalne, zaprojektowane tak, żeby rosnąć w przybliżeniu w miarę upływu czasu.
Przechowywanie to jedna z pierwszych praktycznych różnic: bigint zajmuje 8 bajtów, a UUID 16 bajtów. Ta różnica pojawia się w indeksach i może wpływać na trafienia w cache (mniej wpisów indeksu mieści się w pamięci).
Pomyśl też, gdzie ID pojawiają się poza bazą. Biginty są krótkie w URL-ach i łatwe do przeczytania w logach czy zgłoszeniach. UUIDy są dłuższe i trudniejsze do wpisania, ale trudniej je zgadnąć i można je bezpiecznie generować po stronie klienta.
Rozmiar indeksu i rozrost tabel: co się zmienia
Największa praktyczna różnica między bigint a UUID to rozmiar. Bigint to 8 bajtów; UUID to 16 bajtów. Brzmi to niewiele, dopóki nie przypomnisz sobie, że indeksy powielają Twoje ID wiele razy.
Indeks klucza głównego musi być „gorący” w pamięci, żeby działać szybko. Mniejszy indeks oznacza, że więcej jego części mieści się w shared buffers i cache CPU, więc wyszukiwania i złączenia wymagają mniej odczytów z dysku. Przy kluczach UUID indeks jest zwykle zauważalnie większy dla tej samej liczby wierszy.
Mnożniki to indeksy wtórne. W PostgreSQL B-tree każdy wpis indeksu wtórnego również przechowuje wartość klucza głównego (żeby odnaleźć wiersz). Więc szersze ID zwiększają nie tylko indeks PK, ale każdy dodatkowy indeks. Jeśli masz trzy indeksy wtórne, dodatkowe 8 bajtów z UUID pojawia się skutecznie w czterech miejscach.
Klucze obce i tabele złączeń też to odczują. Każda tabela, która odnosi się do Twojego ID, przechowuje tę wartość we własnych wierszach i indeksach. Tabela many-to-many może składać się głównie z dwóch kluczy obcych plus trochę narzutu, więc podwojenie szerokości klucza może znacząco zmienić jej rozmiar.
W praktyce:
- UUIDy zwykle zwiększają rozmiar indeksów głównych i wtórnych, a różnica narasta wraz z liczbą indeksów.
- Większe indeksy oznaczają większe obciążenie pamięci i więcej odczytów stron pod obciążeniem.
- Im więcej tabel odwołuje się do klucza (events, logs, tabele złączeń), tym bardziej znacząca staje się różnica w rozmiarze.
Jeśli ID użytkownika pojawia się w users, orders, order_items i audit_log, ta sama wartość jest przechowywana i indeksowana we wszystkich tych tabelach. Wybór szerszego ID to równie dobrze decyzja o przestrzeni dyskowej, co decyzja o ID.
Kolejność sortowania i wzorce zapisu: sekwencyjne vs losowe ID
Większość kluczy głównych PostgreSQL leży w indeksie B-tree. B-tree działa najlepiej, gdy nowe wiersze trafiają blisko końca indeksu, bo baza może dokładać wpisy bez dużych przemieszczeń.
Sekwencyjne ID: przewidywalne i przyjazne dla przechowywania
Przy bigint z kolumną identity lub sekwencją nowe ID rosną z czasem. Wstawienia zwykle trafiają na prawą stronę indeksu, więc strony pozostają zwarte, cache jest ciepły, a PostgreSQL wykonuje mniej dodatkowej pracy.
To ma znaczenie nawet jeśli nigdy nie robisz ORDER BY id. Ścieżka zapisu i tak musi umieścić każdy nowy klucz w indeksie w kolejności sortowanej.
Losowe UUIDy: większy rozrzut, więcej przebudowy
Losowy UUID (często UUIDv4) rozrzuca inserty po całym indeksie. To zwiększa prawdopodobieństwo podziałów stron, gdzie PostgreSQL musi zaalokować nowe strony indeksu i przesuwać wpisy, żeby zrobić miejsce. Efektem jest większa amplifikacja zapisu: więcej bajtów indeksu do zapisania, więcej generowanego WAL i częściej praca tła (vacuum i zarządzanie bloatem).
UUIDy porządkujące po czasie zmieniają tę historię. UUIDy, które w przybliżeniu rosną w czasie (np. UUIDv7 lub inne schematy oparte na czasie) przywracają dużą część lokalności, mimo że dalej mają 16 bajtów i wyglądają jak UUIDy w API.
Te różnice odczujesz najbardziej przy dużych szybkościach wstawek, dużych tabelach, które nie mieszczą się w pamięci, oraz przy wielu indeksach wtórnych. Jeśli jesteś wrażliwy na skoki opóźnień zapisu spowodowane podziałami stron, unikaj w pełni losowych ID dla gorących tabel zapisu.
Przykład: ruchliwa tabela zdarzeń przyjmująca logi z aplikacji mobilnej przez cały dzień zazwyczaj działa płynniej przy kluczach sekwencyjnych lub UUIDach porządkujących czas niż przy całkowicie losowych UUIDach.
Rzeczywisty wpływ na wydajność
Większość realnych spowolnień to nie „UUIDy są wolne” ani „biginty są szybkie”. To to, co baza musi dotknąć, aby odpowiedzieć na zapytanie.
Plany zapytań zależą głównie od tego, czy mogą użyć skanu indeksu do filtrów, robić szybkie złączenia po kluczu i czy tabela jest fizycznie uporządkowana (lub na tyle bliska), by odczyty zakresowe były tanie. Z kluczem bigint nowe wiersze trafiają w przybliżeniu w kolejności rosnącej, więc indeks PK zwykle pozostaje zwarty i przyjazny lokalności. Przy losowych UUIDach inserty rozrzucają się po indeksie, co może tworzyć więcej podziałów stron i bardziej chaotyczny porządek na dysku.
Odczyty to miejsce, gdzie wiele zespołów zauważa to jako pierwsze. Większe klucze to większe indeksy, a większe indeksy to mniej stron mieszczących się w pamięci, więc trafienia w cache spadają, a IO rośnie — zwłaszcza przy ekranach łączących dużo danych, jak „lista zamówień z informacją o kliencie”. Jeśli zestaw roboczy nie mieści się w pamięci, schema z UUIDami może szybciej przekroczyć ten próg.
Zapisy też mogą się zmieniać. Losowe inserty UUID mogą zwiększać churn indeksu, co obciąża autovacuum i objawia się skokami opóźnień w okresach dużego ruchu.
Jeśli robisz benchmark UUID vs bigint w PostgreSQL, rób to uczciwie: ta sama schemat, te same indeksy, ten sam fillfactor i wystarczająco dużo wierszy, by przekroczyć RAM (nie 10k). Mierz p95 latency i IO, testuj zarówno ciepły, jak i zimny cache.
Jeśli budujesz aplikacje w AppMaster na PostgreSQL, często objawia się to jako wolniejsze strony list i większe obciążenie bazy długo zanim problem stanie się "problemem CPU".
Bezpieczeństwo i wygoda w systemach publicznych
Jeśli Twoje ID wychodzą z bazy i pojawiają się w URL-ach, odpowiedziach API, zgłoszeniach wsparcia i ekranach mobilnych, wybór wpływa na bezpieczeństwo i codzienną użyteczność.
Biginty są proste dla ludzi. Są krótkie, możesz je przeczytać przez telefon, a zespół wsparcia szybko zauważy wzorce typu „wszystkie awarie są wokół 9 200 000”. To przyspiesza debugowanie, szczególnie gdy pracujesz z logami lub zrzutami ekranu klientów.
UUIDy pomagają, gdy ujawniasz identyfikatory publicznie. UUID trudno odgadnąć, więc casualowe skrobanie jak /users/1, /users/2 nie zadziała. Trudniej też wnioskować, ile rekordów masz.
Pułapką jest myślenie, że „niezagadkiwalny” = „bezpieczny”. Jeśli sprawdzanie uprawnień jest słabe, przewidywalne biginty można łatwo nadużyć, ale UUIDy też mogą zostać skradzione z udostępnionego linku, wycieku logów czy cache’a. Bezpieczeństwo musi pochodzić z kontroli uprawnień, nie z ukrywania ID.
Podejście praktyczne:
- Wymuszaj sprawdzenie własności i role przy każdym odczycie i zapisie.
- Jeśli ujawniasz ID w publicznych API, używaj UUIDów lub oddzielnych publicznych tokenów.
- Jeśli chcesz czytelnych referencji, zachowaj wewnętrzny bigint dla operacji wewnętrznych.
- Nie koduj w ID żadnego wrażliwego znaczenia (np. typu użytkownika).
Przykład: portal klienta pokazuje ID faktur. Jeśli faktury mają bigint, a API tylko sprawdza „faktura istnieje”, ktoś może iterować numery i pobierać cudze faktury. Napraw najpierw kontrolę, potem zdecyduj, czy UUIDy dla publicznych ID zmniejszą ryzyko i obciążenie wsparcia.
W platformach takich jak AppMaster, gdzie ID płyną przez generowane API i aplikacje mobilne, bezpieczniejszym domyślnym wyborem jest konsekwentna autoryzacja plus format ID, który klienci potrafią niezawodnie obsłużyć.
Jak ID przepływają przez API i aplikacje mobilne
Typ bazy danych, który wybierzesz, nie zostaje tylko w bazie. Przenika do wszystkich granic: URL-i, payloadów JSON, lokalnego przechowywania, logów i analityki.
Jeśli kiedykolwiek zmienisz typ ID później, awarie rzadko są „tylko migracją”. Klucze obce muszą zmienić się wszędzie, nie tylko w tabeli głównej. ORMy i generatory kodu mogą zregenerować modele, ale integracje dalej będą oczekiwać starego formatu. Nawet prosty GET /users/123 staje się problematyczny, gdy ID zmienia się w 36-znakowy UUID. Trzeba też zaktualizować cache, kolejki wiadomości i każde miejsce, gdzie ID były przechowywane jako liczby.
Dla API największy wybór to format i walidacja. Biginty przesyła się jako liczby, ale niektóre systemy (i języki) mają problemy z precyzją przy bardzo dużych wartościach, jeśli parsują je jako float. UUIDy przesyła się jako stringi — bezpieczniejsze do parsowania, ale potrzebujesz ścisłej walidacji, żeby uniknąć "prawie-UUID" śmieci w logach i bazie.
Na mobilnych klientach ID są ciągle serializowane i zapisywane: odpowiedzi JSON, lokalne SQLite i kolejki offline przechowujące akcje do czasu przywrócenia sieci. Liczbowe ID są mniejsze, ale stringowe UUIDy są często łatwiejsze do traktowania jako niejawne tokeny. Prawdziwy ból powoduje niekonsekwencja: jedna warstwa przechowuje ID jako integer, inna jako tekst — porównania i złączenia stają się kruche.
Kilka zasad, które ratują zespoły przed problemami:
- Wybierz jedną kanoniczną reprezentację dla API (często string) i trzymaj się jej.
- Waliduj ID na krawędzi i zwracaj jasne błędy 400.
- Przechowuj tę samą reprezentację w lokalnych cache’ach i kolejkach offline.
- Loguj ID używając spójnych nazw pól i formatów w usługach.
Jeśli budujesz web i mobilne klienty za pomocą stosu generowanego (np. AppMaster generujący backend i aplikacje natywne), stabilny kontrakt ID ma jeszcze większe znaczenie, bo staje się częścią każdego wygenerowanego modelu i żądania.
Przygotowanie do shardingu i systemów rozproszonych
„Gotowość do shardingu” to głównie możliwość tworzenia ID w wielu miejscach bez utraty unikalności oraz możliwość przenoszenia danych między węzłami później bez przepisywania wszystkich kluczy obcych.
UUIDy są popularne w setupach multi-region lub multi-writer, bo każdy węzeł może wygenerować unikalne ID bez pytania centralnej sekwencji. To zmniejsza koordynację i ułatwia przyjmowanie zapisów w różnych regionach i późniejsze łączenie danych.
Bigint też działa, ale wymaga planu. Typowe opcje to przydzielanie zakresów numerycznych na shard (shard 1 używa 1–1B, shard 2 1B–2B), oddzielne sekwencje z prefiksem sharda albo użycie generatorów w stylu Snowflake (bity czasu plus identyfikator maszyny). Pozwalają one utrzymać indeksy mniejsze niż UUID, zachować częściowe porządkowanie, ale dodają operacyjne reguły, których trzeba przestrzegać.
Wymiana, która ma znaczenie na co dzień:
- Koordynacja: UUID potrzebuje jej prawie żadnej; bigint często wymaga planowania zakresów lub usługi generatora.
- Kolizje: kolizje UUID są ekstremalnie mało prawdopodobne; bigint jest bezpieczny tylko jeśli zasady przydziału nigdy się nie pokrywają.
- Porządkowanie: wiele schematów bigint jest mniej więcej uporządkowanych czasowo; UUID jest zwykle losowy, chyba że użyjesz wariantu porządkującego czas.
- Złożoność: sharded bigint pozostaje prosty tylko przy dyscyplinie zespołu.
Dla wielu zespołów „gotowość do shardingu” naprawdę oznacza „gotowość do migracji”. Jeśli dziś jesteś na jednej bazie, wybierz ID, które ułatwi obecną pracę. Jeśli budujesz już wiele writerów (np. poprzez generowane API i aplikacje mobilne w AppMaster), ustal wczesną strategię tworzenia i walidacji ID między usługami.
Krok po kroku: wybór strategii ID
Zacznij od nazwania rzeczywistego kształtu Twojej aplikacji. Pojedyncza baza PostgreSQL w jednym regionie ma inne potrzeby niż system multi-tenant, konfiguracja, którą później podzielisz na regiony, czy aplikacja mobilna, która musi tworzyć rekordy offline i synchronizować je później.
Następnie bądź szczery co do tego, gdzie ID będą się pojawiać. Jeśli identyfikatory zostają tylko wewnątrz backendu (zadania, narzędzia administracyjne), prostota często wygrywa. Jeśli ID pojawiają się w URL-ach, logach udostępnionych klientom, zgłoszeniach wsparcia lub deep linkach mobilnych, przewidywalność i prywatność stają się ważniejsze.
Użyj porządkowania jako czynnika decydującego, a nie dodatku. Jeśli polegasz na „najnowsze najpierw” w feedach, stabilnej paginacji czy audytach, sekwencyjne ID (lub ID porządkujące się czasowo) zmniejszają niespodzianki. Jeśli porządkowanie nie jest związane z kluczem głównym, możesz trzymać wybór PK oddzielnie i sortować po znaczniku czasu.
Praktyczny przepływ decyzji:
- Sklasyfikuj architekturę (jedna baza, multi-tenant, multi-region, offline-first) i czy możesz później scalać dane z wielu źródeł.
- Zdecyduj, czy ID są identyfikatorami publicznymi, czy tylko wewnętrznymi.
- Potwierdź potrzeby sortowania i paginacji. Jeśli potrzebujesz kolejności wstawienia, unikaj w pełni losowych ID.
- Jeśli wybierasz UUIDy, celowo wybierz wersję: losową (v4) dla nieprzewidywalności lub porządkującą czasowo dla lepszej lokalności indeksu.
- Zablokuj konwencje wcześnie: jedna kanoniczna forma tekstowa, zasady wielkości liter, walidacja i sposób, w jaki każde API zwraca i przyjmuje ID.
Przykład: jeśli aplikacja mobilna tworzy „zamówienia robocze” offline, UUIDy pozwalają urządzeniu generować ID bezpiecznie zanim serwer je zobaczy. W narzędziach takich jak AppMaster to także wygoda, bo ten sam format ID może płynąć od bazy przez API do aplikacji web i natywnych bez specialnego traktowania.
Częste błędy i pułapki do unikania
Większość debat o ID idzie źle, bo ludzie wybierają typ ID z jednego powodu, a potem dziwią się efektom ubocznym.
Jednym częstym błędem jest używanie całkowicie losowych UUIDów w tabeli o dużym zapisie i zastanawianie się, dlaczego inserty są skokowe. Losowe wartości rozrzucają nowe wiersze po indeksie, co prowadzi do większej liczby podziałów stron i pracy dla bazy pod dużym obciążeniem. Jeśli tabela jest write-heavy, pomyśl o lokalności insercji przed podjęciem decyzji.
Kolejny problem to mieszanie typów ID między usługami i klientami. Na przykład jedna usługa używa bigint, inna UUID, a Twoje API kończy z oboma formatami. To prowadzi do subtelnych bugów: parsery JSON tracą precyzję dużych liczb, kod mobilny traktuje ID jako liczby w jednym widoku, a jako stringi w innym, albo klucze cache nie pasują.
Trzecia pułapka to traktowanie „niezgadywalnych ID” jako kontroli dostępu. Nawet przy UUIDach potrzebujesz poprawnych uprawnień.
Wreszcie, zespoły zmieniają typ ID późno bez planu. Najtrudniejsza część nie jest samym kluczem głównym, ale wszystkim, co jest do niego przypięte: klucze obce, tabele złączeń, URL-e, zdarzenia analityczne, deep linki mobilne i stan przechowywany po stronie klienta.
Aby uniknąć bólu:
- Wybierz jeden typ ID dla publicznych API i trzymaj się go.
- Traktuj ID jako niejawne stringi w klientach, aby unikać problemów liczbowych.
- Nigdy nie polegaj na losowości ID jako kontroli dostępu.
- Jeśli musisz migrować, wersjonuj API i zaplanuj obsługę długowiecznych klientów.
Jeśli budujesz z użyciem platformy generującej kod jak AppMaster, konsekwencja ma jeszcze większe znaczenie, bo ten sam typ ID płynie ze schematu bazy do wygenerowanego backendu i aplikacji web oraz mobilnych.
Krótka lista kontrolna przed decyzją
Jeśli utknąłeś, nie zaczynaj od teorii. Zacznij od obrazu produktu za rok i miejsc, w których ID będzie się pojawiać.
Zapytaj:
- Jak duże będą największe tabele za 12–24 miesiące i czy zachowacie historię latami?
- Czy potrzebujesz ID, które mniej więcej sortują się po czasie utworzenia dla paginacji i debugowania?
- Czy więcej niż jeden system będzie tworzył rekordy jednocześnie, włącznie z aplikacjami offline lub zadaniami backgroundowymi?
- Czy ID pojawi się w URL-ach, zgłoszeniach wsparcia, eksportach lub zrzutach ekranu udostępnianych przez klientów?
- Czy każdy klient (web, iOS, Android, skrypty) potrafi traktować ID w ten sam sposób, włączając walidację i przechowywanie?
Po odpowiedziach sprawdź rury. Jeśli używasz bigint, upewnij się, że masz jasny plan generacji ID we wszystkich środowiskach (zwłaszcza lokalnym dev i przy importach). Jeśli używasz UUID, upewnij się, że kontrakty API i modele klientów obsługują stringowe ID konsekwentnie i że zespół potrafi je czytać i porównywać.
Szybki test praktyczny: jeśli aplikacja mobilna musi stworzyć zamówienie offline i zsynchronizować je później, UUIDy często zmniejszają potrzebę koordynacji. Jeśli aplikacja działa głównie online i chcesz prostych, kompaktowych indeksów, bigint jest zwykle łatwiejszy.
Jeśli budujesz w AppMaster, zdecyduj wcześnie, bo konwencja ID przepłynie przez model PostgreSQL, generowane API i klientów mobilnych/webowych.
Realistyczny scenariusz przykładowy
Mała firma ma narzędzie operacyjne, portal klienta i aplikację mobilną dla pracowników terenowych. Wszystkie trzy korzystają z tej samej bazy PostgreSQL przez jedno API. Nowe rekordy powstają cały dzień: zgłoszenia, zdjęcia, aktualizacje statusu i faktury.
Przy bigint ładunki API są kompaktowe i czytelne:
{ "ticket_id": 4821931, "customer_id": 91244 }
Paginacja jest naturalna: ?after_id=4821931&limit=50. Sortowanie po id zwykle odpowiada czasowi utworzenia, więc „najnowsze zgłoszenia” jest szybkie i przewidywalne. Debugowanie jest też proste: wsparcie może poprosić o „ticket 4821931” i większość osób wpisze go bez błędu.
Przy UUIDach payloady są dłuższe:
{ "ticket_id": "3f9b3c0a-7b9c-4bf0-9f9b-2a1b3c5d1d2e" }
Jeśli używasz losowego UUID v4, inserty trafiają po całym indeksie. To może oznaczać więcej churnu indeksu i nieco mniej wygodne debugowanie (kopiuj/wklej staje się normą). Paginacja często przechodzi na kursory zamiast after id.
Jeśli użyjesz UUID porządkującego czasowo, zachowujesz większość zachowania „najnowsze najpierw”, a jednocześnie unikasz łatwej enumeracji publicznych ID.
W praktyce zespoły zwykle zauważają cztery rzeczy:
- Jak często ID są wpisywane ręcznie vs kopiowane
- Czy „sort by id” odpowiada „sort by created”
- Jak czysta i stabilna jest paginacja kursora
- Jak łatwo śledzić rekord w logach, wywołaniach API i ekranach mobilnych
Następne kroki: wybierz domyślność, przetestuj i ustandaryzuj
Większość zespołów utknie, bo szukają idealnej odpowiedzi. Nie potrzebujesz perfekcji. Potrzebujesz domyślu, który pasuje do produktu dziś i szybkiego sposobu, by udowodnić, że nie zaszkodzi później.
Reguły do ustandaryzowania:
- Używaj bigint, gdy chcesz najmniejszych indeksów, przewidywalnego porządku i prostego debugowania.
- Używaj UUID, gdy ID muszą być trudne do odgadnięcia w URL-ach, spodziewasz się tworzenia offline (mobilne) lub chcesz mniej kolizji między systemami.
- Jeśli możesz dzielić dane według tenantów lub regionów, preferuj plan ID działający między węzłami (UUID lub skoordynowany schemat bigint).
- Wybierz jedno jako domyślny i traktuj wyjątki jako rzadkie. Konsekwencja często wygrywa nad mikrooptymalizacją pojedynczej tabeli.
Zanim to utrwalisz, zrób mały spike. Stwórz tabelę o realistycznym rozmiarze wiersza, wstaw 1–5 milionów wierszy i porównaj (1) rozmiar indeksu, (2) czas insercji, (3) kilka typowych zapytań z kluczem głównym i kilkoma indeksami wtórnymi. Zrób to na rzeczywistym sprzęcie i z rzeczywistym kształtem danych.
Jeśli obawiasz się zmiany później, zaplanuj migrację, żeby była nudna:
- Dodaj nową kolumnę ID i unikalny indeks.
- Dual-write: wypełniaj oba ID dla nowych wierszy.
- Backfilluj stare wiersze partiami.
- Zaktualizuj API i klientów, aby akceptowały nowe ID (utrzymaj stare działające w okresie przejściowym).
- Przełącz czytania, potem usuń stary klucz, gdy logi i metryki będą czyste.
Jeśli budujesz na AppMaster (appmaster.io), warto zdecydować wcześnie, bo konwencja ID przepłynie przez model PostgreSQL, generowane API i aplikacje web oraz natywne. Typ ma znaczenie, ale spójność liczy się bardziej, gdy masz prawdziwych użytkowników i wielu klientów.
FAQ
Domyślnie wybierz bigint, gdy masz jedną bazę PostgreSQL, większość zapisów odbywa się po stronie serwera, a zależy Ci na kompaktowych indeksach i przewidywalnym nadawaniu identyfikatorów. Wybierz UUID, gdy identyfikatory muszą być generowane w wielu miejscach (wiele usług, aplikacje offline, przyszły sharding) lub gdy nie chcesz, żeby publiczne ID było łatwe do odgadnięcia.
Bo wartość ID jest kopiowana w wielu miejscach: indeksie klucza głównego, w każdym indeksie wtórnym (jako wskaźnik do wiersza), w kolumnach kluczy obcych w innych tabelach oraz w tabelach złączeń. UUID zajmuje 16 bajtów kontra 8 bajtów dla bigint, więc różnica rozchodzi się po całym schemacie i może obniżać trafienia w cache.
W tabelach o dużej liczbie zapisów — tak. Losowe UUID (np. v4) rozrzucają inserty po całym B-tree, co zwiększa liczbę podziałów stron i churn indeksu pod obciążeniem. Jeśli chcesz UUID, ale też gładkie zapisy, użyj strategii opierającej się na UUID z porządkiem czasowym, aby nowe klucze trafiały głównie na koniec.
Często objawia się to większym IO, nie wolniejszym CPU. Większe klucze to większe indeksy, a większe indeksy to mniej stron w pamięci, więc złączenia i wyszukiwania mogą powodować więcej odczytów. Różnicę widać najbardziej przy dużych tabelach, zapytaniach intensywnie wykorzystujących złączenia i gdy zestaw roboczy nie mieści się w RAMie.
UUIDy utrudniają odgadywanie zasobów (np. /users/1), ale nie zastępują autoryzacji. Jeśli sprawdzanie uprawnień jest błędne, UUIDy też mogą zostać wykradzione i użyte. Traktuj UUIDy jako wygodę dla publicznych identyfikatorów, a bezpieczeństwo zapewniaj przez rygorystyczne kontrole dostępu.
Ustal jedno kanoniczne przedstawienie i trzymaj się go. Praktycznym domyślnym wyborem jest traktowanie ID jako stringów w żądaniach i odpowiedziach API, nawet jeśli baza używa bigint — unika to problemów z precyzją po stronie klienta. Cokolwiek wybierzesz, bądź konsekwentny w webie, mobilnych klientach, logach i cache.
Bigint może sprawiać problemy w niektórych klientach, jeśli jest parsowany jako liczba zmiennoprzecinkowa i traci precyzję przy bardzo dużych wartościach. UUIDy tego unikają, bo są stringami, ale są dłuższe i łatwiej je źle obsłużyć bez walidacji. Najbezpieczniejsze jest konsekwentne podejście: jeden typ wszędzie i jasna walidacja na krawędzi API.
UUIDy są prostszą opcją, bo można je tworzyć niezależnie na dowolnym węźle bez centralnej sekwencji. Bigint też się sprawdzi, ale potrzebne są reguły — zakresy per shard, prefiksy, albo generator w stylu Snowflake — i trzeba ich zawsze przestrzegać. Jeśli chcesz najprostszy rozdzielony scenariusz, wybierz UUIDy (najlepiej z porządkiem czasowym).
Zmiana typu klucza głównego wpływa na znacznie więcej niż jedną kolumnę. Trzeba zaktualizować klucze obce, tabele złączeń, kontrakty API, przechowywane dane po stronie klientów, wydarzenia analityczne i integracje, które zapisywały ID jako liczby lub stringi. Jeśli możesz potrzebować zmiany, zaplanuj stopniową migrację z dual-write i długim okresem przejściowym.
Możesz używać obu: trzymaj wewnętrzny, kompaktowy bigint dla efektywności bazy, a jako publiczny identyfikator wystawiaj oddzielny UUID (lub token). Dzięki temu masz małe indeksy i wygodne debugowanie wewnętrzne, a jednocześnie unikasz łatwej enumeracji publicznych identyfikatorów. Kluczowe jest wczesne ustalenie, który jest „publicznym ID” i konsekwencja.


