PostgreSQL dla railsowców: JSONB nadszedł
Postgres 9.4 wnosi JSONB — indeksowalny, odpytywalny JSON wewnątrz kolumny relacyjnej. Deep-dive o tym, kiedy po niego sięgać, jak go odpytywać i indeksować, integracji z Rails 4.2 i o pułapce traktowania bazy SQL jak magazynu dokumentów.
Parę lat temu „potrzebuję elastycznych, schemaless pól” było częstym powodem, dla
którego zespoły sięgały po MongoDB. Postgres 9.4, wydany pod koniec zeszłego roku,
odgryza spory kęs z tego argumentu. Jego nowy typ jsonb przechowuje JSON w formacie
binarnym, który jest indeksowalny i odpytywalny — schemaless dane dokumentowe
żyjące wewnątrz kolumny w skądinąd w pełni relacyjnej, transakcyjnej tabeli. Dla
programistów Rails, w parze ze wsparciem JSON, które weszło w 4.2, to jedna z
cichszej ważnych funkcji od lat. Oto jak używać jej dobrze i gdzie nie.
json vs jsonb: zawsze sięgaj po jsonb
Postgres ma teraz dwa typy JSON, a rozróżnienie ma znaczenie. Oryginalny typ json
przechowuje dokładną kopię tekstową tego, co wstawiłeś — białe znaki, kolejność
kluczy, zduplikowane klucze i wszystko. Parsuje ponownie przy każdej operacji i nie
da się go sensownie zindeksować. jsonb parsuje JSON raz przy zapisie i przechowuje
w rozłożonej formie binarnej: kolejność kluczy nie jest zachowana, duplikaty są
usuwane i — co kluczowe — może być wydajnie indeksowany i odpytywany.
Wymiana jest maleńka: zapisy jsonb są nieznacznie wolniejsze (parsuje z góry) i nie
zachowuje formatowania w obie strony. W zamian dostajesz szybkie zapytania o
zawieranie i indeksowanie. W zasadzie dla każdego przypadku użycia aplikacji używaj
jsonb. Zwykły typ json jest do rzadkiego przypadku, gdy musisz zachować
dokładne dane wejściowe bajt po bajcie.
Pierwsza kolumna
W Rails 4.2 kolumna jsonb to pierwszoklasowy typ migracji:
class AddPreferencesToUsers < ActiveRecord::Migration
def change
add_column :users, :preferences, :jsonb, null: false, default: {}
add_index :users, :preferences, using: :gin
end
end
user.preferences = { "theme" => "dark", "notifications" => { "email" => true } }
user.save
user.preferences["theme"] # => "dark" — Rails rzutuje to na hash Ruby za ciebie
Dwa szczegóły warte skopiowania. Nadaj kolumnie default: {} i null: false, byś
nigdy nie musiał sprawdzać nil — każdy wiersz ma co najmniej pusty obiekt. I dodaj
indeks GIN od początku, jeśli będziesz odpytywać w głąb, bo to on czyni operatory
zawierania szybkimi.
Odpytywanie wewnątrz dokumentu
Tu jsonb zarabia na swoje miejsce — możesz odpytywać w głąb struktury z SQL, a
nie tylko pobrać blob i sparsować w Ruby. Podstawowe operatory:
-- -> zwraca wartość JSON, ->> zwraca tekst
SELECT preferences -> 'notifications' ->> 'email' FROM users;
-- @> "zawiera": czy kolumna zawiera ten fragment? (indeksowalne GIN)
SELECT * FROM users WHERE preferences @> '{"theme": "dark"}';
-- ? czy istnieje ten klucz najwyższego poziomu?
SELECT * FROM users WHERE preferences ? 'beta_access';
Z Railsów wrzucasz je do where:
User.where("preferences @> ?", { theme: "dark" }.to_json)
User.where("preferences ->> 'theme' = ?", "dark")
Gwiazdą jest operator zawierania @>, bo to jego przyspiesza indeks GIN. GIN
(Generalized Inverted Index) na kolumnie jsonb indeksuje jej klucze i wartości, by
zapytanie o zawieranie nie skanowało każdego wiersza. Jeśli używasz tylko operatorów
zawierania i istnienia klucza, bardziej kompaktowy wariant GIN jsonb_path_ops jest
mniejszy i jeszcze szybszy:
CREATE INDEX idx_users_prefs ON users USING gin (preferences jsonb_path_ops);
Bez indeksu @> działa, ale skanuje tabelę; z nim masz indeksowane wyszukiwania w
głąb półustrukturyzowanych danych — rzecz, która kiedyś wymagała opuszczenia
Postgresa.
Integracja z Rails: store_accessor
Dla znanego zbioru atrybutów żyjących wewnątrz kolumny jsonb, store_accessor daje
ci prawdziwe metody atrybutów wsparte JSON-em, więc reszta twojego kodu nie musi
wiedzieć, że rozmawia z kolumną JSON:
class User < ActiveRecord::Base
store_accessor :preferences, :theme, :locale, :timezone
end
user.theme = "dark" # zapisuje preferences["theme"]
user.theme # => "dark"
user.locale = "en"
To czysty sposób dodawania „miękkich” kolumn — elastycznych atrybutów per rekord, które dodasz bez migracji — wciąż czytając i zapisując je jak zwykłe pola.
Kiedy używać i jakiej pułapki unikać
To ważna część, bo jsonb łatwo nadużyć. Właściwy instynkt: kolumna jsonb
uzupełnia twój schemat; nie zastępuje go. Sięgaj po niego, gdy:
- Dane są naprawdę zmienne lub rzadkie — preferencje użytkownika, niestandardowe atrybuty per produkt, ustawienia różniące się rekordem, payloady API stron trzecich, które chcesz zachować dosłownie.
- Chcesz dodawać pola bez migracji podczas szybkiej iteracji, a potem awansować te, które się ustabilizują, do prawdziwych kolumn później.
- Przechowujesz dokument, który głównie czytasz i zapisujesz w całości i tylko sporadycznie odpytujesz w głąb.
Nie używaj go jako substytutu porządnych kolumn dla danych ustrukturyzowanych i
relacyjnych. Pułapka jest uwodzicielska: zrzuć wszystko do jednego bloba jsonb i
pomiń projektowanie schematu. Zapłacisz za to później. Kolumny wewnątrz JSON nie mają
kluczy obcych, NOT NULL ani ograniczeń CHECK, słabsze gwarancje typów i
zapytania niezgrabniejsze oraz trudniejsze do optymalizacji niż zwykła indeksowana
kolumna. Dyscyplina to pytanie o każde pole: czy to pierwszoklasowy, odpytywany,
ograniczany atrybut wiersza? Jeśli tak, to kolumna. Jeśli naprawdę swobodny lub
rzadki, to jsonb.
Pięknem Postgresa jest tu to, że nie musisz wybierać globalnie. Ta sama tabela może
mieć sztywne, ograniczane, opatrzone kluczami obcymi kolumny dla ustrukturyzowanego
rdzenia danych oraz kolumnę jsonb dla elastycznych krawędzi — integralność
relacyjną i schemaless elastyczność w jednym transakcyjnym składzie. To naprawdę
lepsza odpowiedź niż uruchamianie osobnej bazy dokumentowej dla elastycznych 10%
twojego modelu, i dlatego „potrzebujemy schemaless pól” nie jest już, samo w sobie,
powodem do opuszczenia Postgresa.
Werdykt
jsonb to jedna z tych funkcji, które po cichu rozszerzają to, co potrafi pojedyncza
baza. Indeksowany, odpytywalny JSON wewnątrz tabeli relacyjnej daje aplikacjom Rails
pragmatyczny środek między sztywnymi kolumnami a magazynem dokumentów, bez kosztu
operacyjnego uruchamiania dwóch baz. Używaj jsonb (nigdy zwykłego json),
indeksuj go GIN-em, sięgaj po store_accessor dla znanych atrybutów i trzymaj
dyscyplinę, że dane ustrukturyzowane należą do kolumn. Użyty tak, to kolejny powód,
dla którego Postgres pozostaje domyślnym wyborem, na którym najchętniej budujemy.