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_01Stare 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.