Skip to content
← Wszystkie wpisy
5 min czytania Dawid Skłodowski

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.