PostgreSQL - jedna baza danych zamiast pięciu osobnych technologii

W typowym startupie architektura wygląda tak: PostgreSQL do danych relacyjnych, Redis do cache'u i kolejek, RabbitMQ albo Kafka do komunikacji między serwisami, MongoDB do "elastycznych" dokumentów, Elasticsearch do wyszukiwania pełnotekstowego. Pięć technologii, pięć serwerów, pięć zestawów monitoringu, pięć punktów awarii.

A co, jeśli powiemy Ci, że PostgreSQL potrafi to wszystko sam?

Nie "jakoś tam". Nie "na małą skalę". PostgreSQL ma wbudowane mechanizmy, które w większości przypadków eliminują potrzebę dodatkowych technologii. Nie dlatego, że jest "wystarczająco dobry" - dlatego, że jest zaprojektowany do tych zadań.

Dlaczego wybieramy PostgreSQL

Zanim przejdziemy do konkretnych funkcji, odpowiedzmy na pytanie fundamentalne: dlaczego akurat PostgreSQL, a nie MySQL, MariaDB, SQL Server?

35 lat rozwoju open source

PostgreSQL powstał w 1986 roku na Uniwersytecie Kalifornijskim w Berkeley. To nie jest produkt jednej firmy - to projekt rozwijany przez globalną społeczność inżynierów, z nową wersją major co rok. Nie ma ryzyka vendor lock-in, nie ma opłat licencyjnych, nie ma "edycji enterprise" z ukrytymi funkcjami.

Standardy SQL na poważnie

PostgreSQL implementuje standard SQL najpełniej ze wszystkich baz danych. Window functions, CTE (Common Table Expressions), LATERAL joins, generowane kolumny, tożsamości (GENERATED ALWAYS AS IDENTITY) - funkcje, które w MySQL pojawiają się lata później albo wcale, w PostgreSQL są od dawna.

Rozszerzalność jako filozofia

PostgreSQL nie jest monolitem - to platforma z systemem rozszerzeń. Możesz dodawać nowe typy danych, operatory, funkcje indeksowania, języki proceduralne. PostGIS, TimescaleDB, pgvector - to nie hacki, to rozszerzenia korzystające z oficjalnego API. Żadna inna baza danych nie ma tak dojrzałego ekosystemu rozszerzeń.

ACID bez kompromisów

Transakcje w PostgreSQL to nie marketingowy checkbox. MVCC (Multi-Version Concurrency Control) zapewnia, że czytelnicy nie blokują pisarzy i pisarze nie blokują czytelników. Transakcyjne DDL oznacza, że nawet ALTER TABLE można cofnąć - spróbuj to zrobić w MySQL.

BEGIN;
ALTER TABLE zamowienia ADD COLUMN priorytet INTEGER DEFAULT 0;
UPDATE zamowienia SET priorytet = 1 WHERE wartosc > 10000;
-- Coś poszło nie tak?
ROLLBACK;  -- ALTER TABLE też się cofnął. W MySQL - nie.

Zamiast Redis: cache, kolejki i pub/sub

Redis to fantastyczne narzędzie. Ale w 80% przypadków firmy wdrażają go do zadań, które PostgreSQL obsługuje natywnie.

LISTEN / NOTIFY - pub/sub w bazie danych

PostgreSQL ma wbudowany mechanizm publish/subscribe. Jeden proces wysyła powiadomienie, wszystkie nasłuchujące połączenia je otrzymują - w czasie rzeczywistym, bez pollingu.

-- Proces A: nasłuchuje
LISTEN nowe_zamowienie;

-- Proces B: wysyła powiadomienie
NOTIFY nowe_zamowienie, '{"id": 12345, "kwota": 1500.00}';

W Elixirze to integruje się natywnie z Phoenix PubSub. Gdy nowe zamówienie trafia do bazy, LiveView automatycznie aktualizuje dashboard - bez Redisa, bez dodatkowej infrastruktury.

Phoenix i Ecto mają wbudowane wsparcie dla LISTEN/NOTIFY. To fundamentalny mechanizm, na którym opiera się real-time w naszych aplikacjach. Jeden INSERT do bazy → powiadomienie → aktualizacja interfejsu u wszystkich użytkowników. Zero dodatkowych serwisów.

Materialized Views - cache, który się sam odświeża

Redis jako cache wymaga logiki inwalidacji: "kiedy usunąć cache?", "co się stanie, gdy cache jest nieaktualny?", "jak rozgrzać cache po restarcie?". To cała warstwa kodu do utrzymania.

PostgreSQL ma materialized views - zapisane wyniki zapytań, które odświeżasz jednym poleceniem:

CREATE MATERIALIZED VIEW raport_sprzedazy AS
SELECT
    date_trunc('month', data_zamowienia) AS miesiac,
    kategoria,
    SUM(wartosc) AS suma,
    COUNT(*) AS liczba_zamowien,
    AVG(wartosc) AS srednia
FROM zamowienia
JOIN produkty USING (produkt_id)
WHERE data_zamowienia >= NOW() - INTERVAL '12 months'
GROUP BY 1, 2;

-- Odświeżenie (np. co godzinę przez pg_cron)
REFRESH MATERIALIZED VIEW CONCURRENTLY raport_sprzedazy;

-- Zapytanie - milisekundy zamiast minut
SELECT * FROM raport_sprzedazy WHERE kategoria = 'elektronika';

CONCURRENTLY oznacza, że view jest dostępny podczas odświeżania. Nie ma downtime'u, nie ma stale data. A wynik jest indeksowany jak zwykła tabela.

UNLOGGED tables - pamięć RAM z trwałością

Potrzebujesz tymczasowego storage'u szybkiego jak Redis, ale z zapytaniami SQL? UNLOGGED tables nie zapisują danych do WAL (Write-Ahead Log), co czyni je wielokrotnie szybsze od zwykłych tabel. Dane przetrwają restart połączenia, ale nie przetrwają crashu serwera - dokładnie jak Redis bez persistencji.

CREATE UNLOGGED TABLE sesje_uzytkownikow (
    token TEXT PRIMARY KEY,
    user_id INTEGER NOT NULL,
    dane JSONB,
    wygasa_o TIMESTAMPTZ NOT NULL
);

-- Indeks na wygasanie - automatyczne czyszczenie przez pg_cron
CREATE INDEX ON sesje_uzytkownikow (wygasa_o);

Idealny przypadek: sesje użytkowników, tymczasowe tokeny, dane formularzy wielokrokowych.

Advisory Locks - distributed locking bez Redisa

Redis jest często używany do blokad rozproszonych (SETNX, Redlock). PostgreSQL ma advisory locks - blokady na poziomie aplikacji, które nie blokują żadnych tabel:

-- Zablokuj zasób nr 42 (np. "generowanie raportu miesięcznego")
SELECT pg_try_advisory_lock(42);

-- Zrób robotę...

-- Zwolnij blokadę
SELECT pg_advisory_unlock(42);

W Elixirze z Ecto to jedna linia kodu. Blokada jest automatycznie zwalniana po zamknięciu połączenia - nie ma ryzyka "martwych blokad" jak w Redis, gdy klient crashuje.


Zamiast Kafka / RabbitMQ: kolejki zadań

Kafka i RabbitMQ to potężne systemy kolejkowe. Ale 90% firm używa ich do prostych kolejek zadań: "wyślij email", "wygeneruj PDF", "przetworz płatność". Do tego PostgreSQL w zupełności wystarcza.

SELECT ... FOR UPDATE SKIP LOCKED - kolejka w jednym zapytaniu

To mechanizm, który zmienił zasady gry. Jedno zapytanie SQL implementuje pełnoprawną kolejkę zadań z gwarancją exactly-once delivery:

-- Pobierz następne zadanie z kolejki (atomowo, bez wyścigów)
UPDATE zadania
SET status = 'processing',
    locked_at = NOW(),
    worker_id = 'worker-1'
WHERE id = (
    SELECT id FROM zadania
    WHERE status = 'pending'
    ORDER BY priorytet DESC, created_at ASC
    FOR UPDATE SKIP LOCKED
    LIMIT 1
)
RETURNING *;

FOR UPDATE blokuje wiersz dla innych workerów. SKIP LOCKED pomija zablokowane wiersze zamiast czekać. Rezultat: wielu workerów może jednocześnie pobierać zadania bez konfliktów, bez duplikatów, bez wyścigów.

Oban - production-ready kolejka na PostgreSQL

Oban to biblioteka Elixira, która buduje na SKIP LOCKED pełnoprawny system kolejek:

  • Retry z exponential backoff - zadanie nie powiodło się? Ponów za 15s, 1min, 5min...
  • Harmonogramy (cron) - "co poniedziałek o 8:00 wyślij raport"
  • Priorytety i limity - "max 5 emaili na sekundę"
  • Unikalne zadania - "nie generuj tego samego raportu dwa razy"
  • Historyczność - pełna historia zadań, czasy wykonania, błędy - w SQL

Wszystko w PostgreSQL. Zero zewnętrznych zależności. Oban obsługuje miliony zadań dziennie w produkcji u setek firm. Żadnego Redisa, żadnej Kafki.

Dlaczego to lepsze niż osobna kolejka? Bo Twoje zadania i Twoje dane są w tej samej transakcji:

Ecto.Multi.new()
|> Ecto.Multi.insert(:zamowienie, zamowienie_changeset)
|> Oban.insert(:email, WyslijPotwierdzenie.new(%{zamowienie_id: zamowienie.id}))
|> Repo.transaction()

Jeśli INSERT zamówienia się nie powiedzie, zadanie emaila też się nie utworzy. Nie ma sytuacji "zamówienie nie powstało, ale email poszedł" ani "zamówienie powstało, ale email nie poszedł". W systemie z osobną kolejką (Redis, RabbitMQ) musisz sam zadbać o tę spójność - i to jest koszmarnie trudne.

Logical Replication - Change Data Capture

Kafka jest często wdrażana jako CDC (Change Data Capture) - "powiadom mnie, gdy coś się zmieni w bazie". PostgreSQL ma to wbudowane:

Logical replication pozwala streamować zmiany z bazy do dowolnego konsumenta. Każdy INSERT, UPDATE, DELETE jest wysyłany jako event.

ElectricSQL - projekt, który buduje na logical replication synchronizację PostgreSQL z aplikacjami klienckimi. Zmiany w bazie propagują się do przeglądarki w czasie rzeczywistym. Phoenix LiveView + ElectricSQL = offline-first aplikacja bez Kafki.


Zamiast MongoDB: dokumenty w PostgreSQL

MongoDB zyskała popularność hasłem "schemaless" - wrzuć JSON, nie martw się o schemat. Ale prawda jest taka, że PostgreSQL obsługuje JSON lepiej niż MongoDB obsługuje relacje.

JSONB - binarny JSON z pełnym indeksowaniem

PostgreSQL ma typ JSONB - binarną reprezentację JSON z natywnym indeksowaniem i zapytaniami:

CREATE TABLE produkty (
    id SERIAL PRIMARY KEY,
    nazwa TEXT NOT NULL,
    cena NUMERIC NOT NULL,
    -- Elastyczne atrybuty - inne dla każdej kategorii
    atrybuty JSONB DEFAULT '{}'
);

-- Elektronika ma pojemność baterii, rozdzielczość ekranu
INSERT INTO produkty (nazwa, cena, atrybuty) VALUES
('Laptop X', 4500, '{"ram": 16, "dysk": "512GB SSD", "ekran": "15.6 cali"}');

-- Odzież ma rozmiar, kolor, materiał
INSERT INTO produkty (nazwa, cena, atrybuty) VALUES
('Koszulka Y', 89, '{"rozmiar": "L", "kolor": "niebieski", "material": "bawelna"}');

-- Zapytanie po zagnieżdżonym polu JSON
SELECT nazwa, cena FROM produkty
WHERE atrybuty->>'ram' IS NOT NULL
  AND (atrybuty->>'ram')::int >= 16;

-- Indeks GIN na JSONB - szybkie wyszukiwanie po dowolnym kluczu
CREATE INDEX idx_produkty_atrybuty ON produkty USING GIN (atrybuty);

-- Zapytanie z operatorem @> (contains)
SELECT * FROM produkty WHERE atrybuty @> '{"kolor": "niebieski"}';

Masz elastyczność MongoDB (różne atrybuty dla różnych produktów) z gwarancjami PostgreSQL (transakcje, constrainty, joiny z innymi tabelami).

Najlepsza część: relacje + dokumenty razem

W MongoDB, gdy potrzebujesz joinów, zaczynają się schody - $lookup jest wolny i ograniczony. W PostgreSQL łączysz dane relacyjne z dokumentami JSONB w jednym zapytaniu:

SELECT
    z.numer_zamowienia,
    k.nazwa AS klient,
    p.nazwa AS produkt,
    p.atrybuty->>'kolor' AS kolor
FROM zamowienia z
JOIN klienci k ON z.klient_id = k.id
JOIN produkty p ON z.produkt_id = p.id
WHERE p.atrybuty @> '{"rozmiar": "L"}'
  AND z.data >= '2026-01-01';

Relacje tam, gdzie mają sens (zamówienia ↔ klienci). Elastyczność tam, gdzie jest potrzebna (atrybuty produktów). Jedno zapytanie, jedna baza, zero kompromisów.

JSON Path - zaawansowane zapytania

Od PostgreSQL 12 masz JSON Path - standardowy sposób zapytywania zagnieżdżonych struktur JSON:

-- Znajdź produkty, gdzie jakakolwiek specyfikacja zawiera "SSD"
SELECT * FROM produkty
WHERE jsonb_path_exists(atrybuty, '$.** ? (@ like_regex "SSD")');

Zamiast Elasticsearch: wyszukiwanie pełnotekstowe

Elasticsearch to 10GB RAM minimum, klaster do zarządzania, osobna synchronizacja danych. Dla 90% przypadków wyszukiwania PostgreSQL ma wszystko, czego potrzebujesz.

tsvector + tsquery - pełnotekstowe wyszukiwanie

PostgreSQL ma wbudowany silnik wyszukiwania pełnotekstowego z obsługą języka polskiego:

-- Konfiguracja polskiego słownika
ALTER TABLE produkty ADD COLUMN szukaj tsvector
    GENERATED ALWAYS AS (
        to_tsvector('polish', nazwa || ' ' || COALESCE(opis, ''))
    ) STORED;

CREATE INDEX idx_produkty_szukaj ON produkty USING GIN (szukaj);

-- Wyszukiwanie z rankingiem
SELECT nazwa, ts_rank(szukaj, query) AS trafnosc
FROM produkty, to_tsquery('polish', 'laptop & lekki') query
WHERE szukaj @@ query
ORDER BY trafnosc DESC;

Stemming (odmiana wyrazów), ranking trafności, wyszukiwanie frazowe, operatory logiczne (AND, OR, NOT) - wszystko wbudowane. Dla katalogu produktów z 500 000 pozycji to wystarczy z nawiązką.

pg_trgm - wyszukiwanie rozmyte i "czy chodziło o..."

Użytkownik wpisał "laptp" zamiast "laptop"? Rozszerzenie pg_trgm obsługuje wyszukiwanie przez podobieństwo trigramowe:

CREATE EXTENSION pg_trgm;
CREATE INDEX idx_nazwa_trgm ON produkty USING GIN (nazwa gin_trgm_ops);

-- Znajdź produkty z nazwą podobną do "laptp" (literówka)
SELECT nazwa, similarity(nazwa, 'laptp') AS podobienstwo
FROM produkty
WHERE nazwa % 'laptp'
ORDER BY podobienstwo DESC;

Autocomplete, "czy chodziło Ci o...", wyszukiwanie tolerujące błędy - bez Elasticsearch.


Zamiast specjalizowanych baz: PostGIS, pgvector, partycjonowanie

PostGIS - dane geospacyjne

Potrzebujesz "znajdź wszystkie magazyny w promieniu 50km od klienta"? Nie potrzebujesz MongoDB z geospatial queries ani osobnej bazy geodanych. PostGIS to najpotężniejsze rozszerzenie geospacyjne na rynku:

CREATE EXTENSION postgis;

SELECT nazwa, adres,
    ST_Distance(
        lokalizacja::geography,
        ST_MakePoint(21.0122, 52.2297)::geography  -- Warszawa
    ) / 1000 AS odleglosc_km
FROM magazyny
WHERE ST_DWithin(
    lokalizacja::geography,
    ST_MakePoint(21.0122, 52.2297)::geography,
    50000  -- 50km w metrach
)
ORDER BY odleglosc_km;

PostGIS obsługuje dane, których MongoDB nawet nie rozumie: wielokąty, linie, bufory, przecięcia, transformacje układów współrzędnych. NASA, OSM i większość systemów GIS na świecie używa PostGIS.

pgvector - wyszukiwanie wektorowe (AI/ML)

Budujesz wyszukiwarkę semantyczną? Rekomendacje produktów? RAG (Retrieval-Augmented Generation) dla LLM? Nie potrzebujesz Pinecone ani Weaviate:

CREATE EXTENSION vector;

ALTER TABLE produkty ADD COLUMN embedding vector(1536);  -- OpenAI embedding size

-- Znajdź produkty semantycznie podobne do zapytania
SELECT nazwa, cena
FROM produkty
ORDER BY embedding <=> '[0.1, 0.2, ...]'::vector  -- cosine distance
LIMIT 10;

Twoje embeddingi, Twoje dane relacyjne, Twoje filtry - w jednym zapytaniu. Nie musisz synchronizować wektorów między PostgreSQL a osobną bazą wektorową.

Partycjonowanie natywne - dane czasowe

Masz tabelę logów z miliardami wierszy? Dane telemetryczne z IoT? Nie potrzebujesz InfluxDB:

CREATE TABLE logi (
    id BIGSERIAL,
    timestamp TIMESTAMPTZ NOT NULL,
    poziom TEXT,
    wiadomosc TEXT,
    dane JSONB
) PARTITION BY RANGE (timestamp);

-- Partycja na każdy miesiąc
CREATE TABLE logi_2026_01 PARTITION OF logi
    FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
CREATE TABLE logi_2026_02 PARTITION OF logi
    FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');

-- Zapytanie automatycznie skanuje tylko odpowiednią partycję
SELECT * FROM logi
WHERE timestamp >= '2026-01-15' AND timestamp < '2026-01-16';
-- PostgreSQL wie, że wystarczy przeszukać logi_2026_01

Stare partycje można odłączyć (DETACH), zarchiwizować lub usunąć - bez wpływu na resztę tabeli. Zerowy downtime.


Funkcje, których nie ma nigdzie indziej

Transakcyjny DDL

W PostgreSQL zmiany schematu są transakcyjne. Możesz zrobić ALTER TABLE, CREATE INDEX, DROP COLUMN wewnątrz transakcji i cofnąć je ROLLBACK-iem. W MySQL, SQL Server, Oracle - DDL automatycznie commituje transakcję. To oznacza, że migracja bazy danych w PostgreSQL jest bezpieczna - jeśli cokolwiek się nie uda, cała migracja się cofa.

Row-Level Security

Kontrola dostępu na poziomie wiersza - bez zmian w kodzie aplikacji:

-- Każdy użytkownik widzi tylko swoje zamówienia
CREATE POLICY zamowienia_policy ON zamowienia
    USING (klient_id = current_setting('app.current_user_id')::int);

ALTER TABLE zamowienia ENABLE ROW LEVEL SECURITY;

Multi-tenant aplikacja SaaS? Dane klientów są izolowane na poziomie bazy danych. Nawet jeśli programista napisze SELECT * FROM zamowienia - dostanie tylko dane swojego tenanta.

Window Functions na poważnie

PostgreSQL implementuje window functions najpełniej ze wszystkich baz:

-- Ranking sprzedawców w każdym regionie + porównanie z poprzednim miesiącem
SELECT
    sprzedawca,
    region,
    sprzedaz,
    RANK() OVER (PARTITION BY region ORDER BY sprzedaz DESC) AS pozycja,
    sprzedaz - LAG(sprzedaz) OVER (
        PARTITION BY sprzedawca ORDER BY miesiac
    ) AS zmiana_vs_poprzedni
FROM sprzedaz_miesieczna;

Rankingi, narastająco, średnia krocząca, percentyle, porównania okres do okresu - jedno zapytanie SQL. Bez eksportu do Excela, bez kodu w aplikacji.

Foreign Data Wrappers - federacja danych

Musisz odpytać dane z MySQL, pliku CSV, API REST albo innego PostgreSQL? Foreign Data Wrappers tworzą wirtualne tabele:

CREATE EXTENSION postgres_fdw;

-- Podłącz się do zdalnego PostgreSQL
CREATE SERVER stary_system FOREIGN DATA WRAPPER postgres_fdw
    OPTIONS (host 'legacy.firma.pl', dbname 'erp');

-- Importuj tabele
IMPORT FOREIGN SCHEMA public FROM SERVER stary_system INTO legacy;

-- Zapytanie łączące dane z dwóch systemów
SELECT n.klient, n.wartosc, s.historia_kredytowa
FROM nowe_zamowienia n
JOIN legacy.klienci s ON n.nip = s.nip;

Idealny scenariusz: migracja z legacy systemu. Nowa aplikacja w Phoenix działa na swoim PostgreSQL, ale nadal ma dostęp do danych ze starego systemu - bez ETL, bez synchronizacji.

pg_stat_statements - rentgen wydajności

Wbudowana diagnostyka zapytań - które zapytania są najwolniejsze, ile razy się wykonały, ile pamięci zużyły:

SELECT
    query,
    calls,
    mean_exec_time::numeric(10,2) AS sredni_czas_ms,
    total_exec_time::numeric(10,2) AS laczny_czas_ms
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;

Nie potrzebujesz APM (Application Performance Monitoring) do diagnozowania problemów z bazą. PostgreSQL mówi Ci sam, co go boli.


Kiedy PostgreSQL NIE wystarczy

Uczciwie - są scenariusze, w których dedykowane narzędzie ma sens:

  • Kafka - gdy potrzebujesz przetwarzania strumieniowego z milionami eventów na sekundę, replikacji między datacenter, wielodniowej retencji logów. Kafka to log rozproszony, PostgreSQL to baza danych - przy skali Netflixa czy LinkedIn to inna kategoria.
  • Redis - gdy potrzebujesz sub-milisekundowej latencji na operacjach klucz-wartość przy setkach tysięcy zapytań na sekundę. Redis trzyma wszystko w RAM - PostgreSQL nie.
  • Elasticsearch - gdy masz miliardy dokumentów tekstowych i potrzebujesz zaawansowanego NLP, faceted search, agregatów na poziomie klastra.
  • ClickHouse/Druid - gdy robisz analitykę OLAP na petabajtach danych z miliardami wierszy.

Ale bądźmy szczerzy: większość firm nie jest Netflixem. Większość systemów ERP, CRM, e-commerce, SaaS obsługuje tysiące, nie miliony zapytań na sekundę. Na tej skali PostgreSQL nie tylko wystarcza - jest lepszym wyborem, bo masz jedną technologię do opanowania, monitorowania i utrzymywania.

Mniej technologii = mniej problemów

Każda dodatkowa technologia w stacku to:

  • Kolejny serwer do utrzymania, monitorowania i aktualizowania
  • Kolejna synchronizacja danych do zaimplementowania i debugowania
  • Kolejny punkt awarii - co się stanie, gdy Redis padnie? Gdy Kafka straci wiadomość?
  • Kolejna technologia do nauki dla zespołu
  • Kolejny koszt - licencje, infrastruktura, DevOps

PostgreSQL jako centralny punkt danych eliminuje problemy, o których istnieniu dowiadujesz się dopiero o 3 w nocy, gdy monitoring krzyczy, że Kafka consumer lag rośnie, a Redis skończyła się pamięć.

PostgreSQL + Elixir + Rust = kompletny stack

Dlatego nasz stack to:

  • Elixir na BEAM - współbieżność, niezawodność, real-time
  • Rust przez Rustler - wydajność obliczeniowa tam, gdzie BEAM nie wystarczy
  • PostgreSQL - dane, kolejki, cache, wyszukiwanie, dokumenty

Trzy technologie zamiast dziesięciu. Mniej ruchomych części, mniej punktów awarii, mniej nocnych alarmów. System, który robi więcej z mniej.

Chcesz zobaczyć, jak PostgreSQL może zastąpić 3 technologie w Twoim systemie? Porozmawiajmy - pokażemy na Twoich danych, co jest możliwe.