25.09.2025 została wydana nowa wersja Postgresa – PostgreSQL 18. Oto kilka szczegółów na temat najważniejszych i najciekawszych funkcji nowej wersji. Kluczowe nowości silnika PostgreSQL 18 – w naszej ocenie – stanowią w szczególności: asynchroniczne I/O (duży zastrzyk wydajności dla odczytów), natywna obsługa UUID v7 (lepsza „sortowalność” i wzrost wydajności), B-tree skip scan (wykorzystanie indeksu z pominięciem kolumn wstępnych przy zapytaniach z =), wirtualne kolumny generowane (obliczane przy odczycie, bez zapisu na dysk) oraz uwierzytelnianie OAuth 2.0 w pg_hba.conf.
Spis treści:
- Asynchroniczne I/O
- UUID v7
- Rozbudowana klauzula returning
- Kolumny generowane „w locie”
- OAuth 2.0
- Podsumowanie
Poszukujesz wsparcia dla PostgreSQL? Zapoznaj się z naszą ofertą.
Asynchroniczne I/O
PostgreSQL 18 dodaje asynchroniczne I/O. Oznacza to szybsze odczyty w wielu przypadkach użycia. Jest to także element większej serii ulepszeń wydajności planowanych dla przyszłych wydań silnika, z których częścią może być wielowątkowość. Należy spodziewać się dalszych informacji o tym w kolejnych wersjach.
Czym jest asynchroniczne I/O?
Gdy danych nie ma jeszcze we wspólnych buforach pamięci, silnik czyta je z dysku, a do pobrania danych potrzebne jest wykonanie operacji I/O. Synchroniczne I/O oznacza, że każde pojedyncze żądanie skierowane do „dysku” jest „odczekiwane” do zakończenia, zanim proces przejdzie do wykonania kolejnych operacji. Dla baz danych z dużą aktywnością może to stanowić wąskie gardło, które tylko częściowo można zmniejszyć, regulując parametrami konfiguracyjnymi silnika.
PostgreSQL 18 wprowadza asynchroniczne I/O, pozwalając procesom roboczym/workerom/pracownikom lepiej wykorzystać czas bezczynności i poprawić przepustowość systemu poprzez grupowanie odczytów.
Obecnie (w wersjach <=17) silnik polega na systemie operacyjnym przy inteligentnej obsłudze I/O, oczekując od OS lub warstwy dyskowej odczytu z wyprzedzeniem dla skanów sekwencyjnych oraz używając funkcji takich jak linuksowe posix_fadvise dla innych typów odczytu, jak skany indeksów bitmapowych. Przeniesienie tej pracy do bazy, dzięki asynchronicznemu I/O, zapewnia bardziej przewidywalną i wydajniejszą metodę grupowania operacji na poziomie bazy danych. Dodatkowo dostępny będzie nowy widok systemowy pg_aios, dostarczający dane o systemie asynchronicznego I/O.
Zapisy realizowane przez silnik pozostaną synchroniczne — ponieważ jest to potrzebne dla zachowania właściwości ACID.
Asynchroniczne I/O w wersji PostgreSQL 18 wpłynie na zwiększenie wydajności w czasie:
- skanów sekwencyjnych (sequential scans),
- skanów bitmapowych tabel(bitmap heap scans; po skanie indeksu bitmapowego),
- niektórych operacji konserwacyjnych, takich jak np.: vacuum.
Domyślnie silnik włączy io_method = worker z 3 workerami obsługującymi operacje I/O. Wartość tego parametru można regulować w systemach z większą liczbą rdzeni CPU. Aktualnie nie mamy jeszcze wiarygodnych rekomendacji ustawień tego parametru konfiguracyjnego, więc jedyną metodą jest wykonanie testów we własnym zakresie.
Dla silnika PostgreSQL działającego na Linuksie 5.1+ możemy wykorzystać wywołania io_uring i realizować wywołania przez właściwe backendy, zamiast przez oddzielne procesy, za pomocą opcjonalnego ustawienia: io_method = io_uring.
Asynchroniczne I/O — szybki start i inspekcja
-- 1. sprawdź aktualny tryb I/O i liczby pracowników
show io_method; -- spodziewane: 'worker' / 'sync' / 'io_uring'
show io_workers; -- domyślnie 3
-- 2. (po restarcie serwera) włącz 'io_uring' na Linuksie 5.1+ z liburing
alter system set io_method = 'io_uring'; -- wymaga kompilacji z --with-liburing i restartu
-- 3. podnieś liczbę pracowników (bez restartu, jeśli parametr ma SIGHUP)
alter system set io_workers = 6; -- dobierz do liczby rdzeni/obciążenia
-- 4. podgląd trwających operacji AIO
select pid, state, operation, length, result
from pg_aios
order by pid, io_id;
W praktyce AIO przyspiesza m.in. skany sekwencyjne, bitmap heap scans i VACUUM. Warto monitorować wpływ na metryki I/O oraz pamiętać, że zapisy pozostają synchroniczne.
Uwagi
- Jeżeli będziemy korzystali z trybu worker, zaczynamy od domyślnie skonfigurowanego: io_workers = 3 i testujemy wzrostem do 4–8: dla instalacji o dużej dynamice zapytań i różnych wzorcach odczytu; zwiększenie liczby workerów wymaga odpowiedniej liczby rdzeni CPU.
- Dla io_uring należy się upewnić, że jądro i system plików wspierają tę ścieżkę (tj. RHEL 8/9: „…io_uring is a new and effective asynchronous I/O interface, which is now available as a Technology Preview. By default, this feature is disabled„).
Mini-benchmark AIO (worker vs. io_uring) — pgbench
1. Założenia i przygotowanie
- Silnik PostgreSQL 18, Linux (dla io_uring — kernel ≥ 5.1 i Postgres skompilowany z liburing). Możemy spróbować sprawdzić na poziomie systemu operacyjnego:
- grep io_uring_setup /proc/kallsyms,
- grep -i uring /boot/config-$(uname -r).
- Mamy rolę z uprawnieniami superuser do alter system.
- Test robimy w osobnej bazie pgbenchdb.
# 1) utworzenie bazy testowej i inicjalizacja danych
createdb pgbenchdb
pgbench -i -s 20 pgbenchdb # skala 20 (~300k krotek w pgbench_accounts)
# 2) opcjonalnie: wyłączenie autovacuum na czas powtarzalnych testów
psql -d pgbenchdb -v ON_ERROR_STOP=1 <<'SQL'
alter system set autovacuum = off; -- tylko na czas benchmarku
select pg_reload_conf();
SQL
2. Skrypt testowy – 2 przebiegi: worker i io_uring
Uwagi
- Zmiana io_method zwykle wymaga restartu serwera. Skrypt poniżej wypisuje polecenia do wykonania i zatrzymuje się przed startem kolejnego przebiegu, aby w odrębnym oknie można było wykonać kontrolowany restart silnika.
Poniższy skrypt zapisujemy jako bench_aio.sh oraz nadajemy uprawnienia do wykonania: chmod +x bench_aio.sh.
#!/usr/bin/env bash
set -euo pipefail
DB="pgbenchdb"
DUR=${DUR:-120} # czas testu (s)
CLIENTS=${CLIENTS:-32} # liczba klientów
THREADS=${THREADS:-8} # wątki pgbench
SCALE=${SCALE:-20} # skala (dla informacji)
echo "== Parametry: DUR=$DUR CLIENTS=$CLIENTS THREADS=$THREADS SCALE=$SCALE =="
function show_env() {
echo "== Konfiguracja AIO =="
psql -d "$DB" -Atc "show io_method; show io_workers;" | nl -ba
}
function run_pgbench() {
local tag="$1"
echo "== Start pgbench [$tag] =="
# standardowy miks TPC-B
pgbench -d "$DB" -T "$DUR" -c "$CLIENTS" -j "$THREADS" -P 5 | tee "pgbench_${tag}.log"
# obciążenie "bardziej" skanujące (read-only)
pgbench -d "$DB" -T "$DUR" -c "$CLIENTS" -j "$THREADS" -P 5 -S | tee -a "pgbench_${tag}.log"
echo "== Koniec pgbench [$tag] =="
echo
}
echo "##### FAZA 1: io_method=worker (domyślne) ##"
psql -d "$DB" -v ON_ERROR_STOP=1 <<'SQL'
alter system set io_method = 'worker';
alter system set io_workers = 3; -- zaczynamy od domyślnego
select pg_reload_conf();
SQL
show_env
echo "Uruchom ponownie (w odrębnym oknie) silnik **jeśli wymagane** (np. systemctl restart postgresql) i wciśnij [Enter]..."
read -r
run_pgbench "worker"
echo "##### FAZA 2: io_method=io_uring ##"
psql -d "$DB" -v ON_ERROR_STOP=1 <<'SQL'
alter system set io_method = 'io_uring';
alter system set io_workers = 3;
select pg_reload_conf();
SQL
show_env
echo "Uruchom ponownie silnik **wymagane** i wciśnij [Enter]..."
read -r
run_pgbench "io_uring"
echo "== Podsumowanie TPS (linia 'tps = ...') =="
grep -h "tps =" pgbench_worker.log pgbench_io_uring.log | sed 's/^/ /'
echo
echo "Pliki wyników: pgbench_worker.log, pgbench_io_uring.log"
3. Dodatkowe obserwacje podczas testu
W oddzielnej sesji można podglądać aktywność AIO:
-- trwające/ostatnie zdarzenia asynchronicznego I/O
select * from pg_aios order by pid, io_id limit 50;
UUID v7
Identyfikatory UUID w nowej wersji silnika przechodzą małe „odświeżenie”, poprzez przejście na v7.
UUID to losowo generowane ciągi znaków o globalnej unikalności, często używane jako klucze główne. UUID są popularne we współczesnych aplikacjach z kilku powodów:
- są unikalne: możemy używać kluczy generowanych w więcej niż jednym miejscu;
- słabo sprzęgnięte: aplikacja może wygenerować klucz główny przed wysłaniem danych do bazy;
- „zaciemnianie” w URL: jeśli URL-e używają kluczy głównych (np. /users/5), inne typy URL-i łatwo zgadnąć (/users/6, /users/7). Z UUID (/users/f47ac10b-58cc-4372-a567-0e02b2c3d479) zgadywanie innych identyfikatorów jest niemożliwe (lub bardzo mało prawdopodobne).
Nowy standard UUID v7 pojawił się w połowie 2024 r. poprzez serię aktualizacji standardów. Wcześniej natywnie wspierana była wersja v4. Jednak sortowanie i indeksowanie dużych tabel generowało problemy wydajnościowe z powodu losowości, prowadząc do fragmentacji indeksów i słabej lokalności. Zaimplementowany w PostgreSQL 18 UUID v7 pomaga w problemach sortowania i indeksowania. Nadal jest losowy, ale pierwsze 48 bitów (12 znaków) to znacznik czasu, a pozostałe bity są losowe; daje to lepszą lokalność danych wstawianych w zbliżonym czasie, a więc lepszą „indeksowalność”. Lokalność, w kontekście UUID v7 oznacza że im większa lokalność, tym częściej trafiamy w te same strony pamięci/dysku i te same bloki cache, zamiast „skakać” losowo po całym indeksie czy tabeli. W praktyce, otrzymujemy:
- mniej odczytów stron (mniej losowych I/O), częściej występuje skan sekwencyjny zamiast „skakania”;
- lepsze trafienia cache buforów silnika i systemu operacyjnego; Lepsze hit-ratio buforów i niższe latencje dla zapytań po czasie (np. order by id limit 100);
- mniej splitów liści B-tree przy wstawieniach, w efekcie: mniej fragmentacji indeksu i mniejszy rozmiar po czasie intensywnych insertów;
- szybsze zakresowe zapytania (np. „dane z ostatniej godziny”);
- mniej losowych I/O podczas dużych operacji: INSERT/UPDATE (zwłaszcza hot-update).
Część znacznikowa UUID v7 jest w zapisie szesnastkowym (tj. „skompresowana dziesiętna”). Na przykład UUID zaczynający się od 01896d6e4a5d6 (hex) odpowiada 2707238289622 (dec) i jest to liczba milisekund od 1970 r.
Tak będzie wyglądać DDL dla UUID v7:
create table dzialania_uzytkownika (
action_id uuid primary key default uuidv7(),
user_id bigint not null,
action_description text,
action_time timestamptz not null default now()
);
create index idx_action_id on dzialania_uzytkownika (action_id);
UUID v7 — klucze główne i sortowanie po czasie, przykład
-- utwórz schemat na potrzeby magazynu
create schema if not exists magazyn;
-- tabela zdarzeń magazynowych z kluczem głównym uuid v7
create table if not exists magazyn.zdarzenie (
zdarzenie_id uuid primary key default uuidv7(), -- PG18: identyfikator uporządkowany czasowo
dane jsonb not null, -- ładunek/detal zdarzenia (np. kto, ile, skąd-dokąd)
utworzono_at timestamptz not null default now()
);
-- wydajny indeks b-tree po (zdarzenie_id) tworzony współbieżnie
create index concurrently if not exists ix_zdarzenie_id
on magazyn.zdarzenie (zdarzenie_id);
-- przykładowe zdarzenia
insert into magazyn.zdarzenie (dane) values
(jsonb_build_object('typ','PZ','opis','przyjęcie 20 szt. towaru A')),
(jsonb_build_object('typ','WZ','opis','wydanie 5 szt. towaru B')),
(jsonb_build_object('typ','MM','opis','przesunięcie 3 szt. z regału R1 do R2'));
-- 1) „czas” z UUID v7: raport ostatnich zdarzeń z dekodowanym timestampem
select
zdarzenie_id,
uuid_extract_timestamp(zdarzenie_id) as czas_z_uuid, -- znacznik czasu z prefiksu uuid v7
utworzono_at,
dane
from magazyn.zdarzenie
order by czas_z_uuid desc
limit 10;
-- 2) dzięki lokalności uuid v7, najnowsze rekordy dostaniemy także przez sortowanie po samym PK
select
zdarzenie_id, utworzono_at, dane
from magazyn.zdarzenie
order by zdarzenie_id desc -- praktycznie równoważne „po czasie”
limit 10;
-- 3) filtr „z ostatnich 24h” na podstawie czasu z uuid v7
select
zdarzenie_id, uuid_extract_timestamp(zdarzenie_id) as czas_z_uuid, dane
from magazyn.zdarzenie
where uuid_extract_timestamp(zdarzenie_id) >= now() - interval '24 hours'
order by zdarzenie_id desc;
Tak wygląda plan zapytania:
explain select
zdarzenie_id, uuid_extract_timestamp(zdarzenie_id) as czas_z_uuid, dane
from magazyn.zdarzenie
where uuid_extract_timestamp(zdarzenie_id) >= now() - interval '24 hours'
order by zdarzenie_id desc;
QUERY PLAN
----------------------------------------------------------------------------------
Index Scan Backward using ix_zdarzenie_id on zdarzenie (cost=0.15..73.21 rows=323 width=56)
Filter: (uuid_extract_timestamp(zdarzenie_id) >= (now() - '24:00:00'::interval))
(2 rows)
W ostatnim zapytaniu, można dodatkowo utworzyć indeks dla jego optymalizacji:
explain select
zdarzenie_id, uuid_extract_timestamp(zdarzenie_id) as czas_z_uuid, dane
from magazyn.zdarzenie
where uuid_extract_timestamp(zdarzenie_id) >= now() - interval '24 hours'
order by zdarzenie_id desc;
QUERY PLAN
---------------------------------------------------------------------------
Sort (cost=8.16..8.17 rows=1 width=56)
Sort Key: zdarzenie_id DESC
-> Index Scan using ix_zdarzenie_ts on zdarzenie (cost=0.14..8.15 rows=1 width=56)
Index Cond: (uuid_extract_timestamp(zdarzenie_id) >= (now() - '24:00:00'::interval))
(4 rows)
Podsumowując:
- uuidv7() generuje identyfikatory uporządkowane czasowo, co poprawia lokalność w indeksach B-tree. Dostępna jest też funkcja uuid_extract_timestamp() (obsługuje v1 i v7).
B-tree skip scan
W PostgreSQL 18 planer potrafi w wielu sytuacjach użyć wielokolumnowego indeksu B-tree, nawet jeśli w zapytaniu pomijamy kolumnę wiodącą. To optymalizacja znana jako skip scan: silnik „przeskakuje” zakresy indeksu związane z różnymi wartościami kolumn wiodących i skanuje tylko te, które mogą dopasować się do warunku na kolumnie dalszej.
Kiedy działa najlepiej?
- Gdy pominięta kolumna wiodąca ma niską krotność (np. status z kilkoma wartościami: aktywne/wycofane/archiwalne).
- Gdy warunek na kolumnie dalszej używa operatora równości (=). (Skip scan nie obejmuje zakresów ani nierówności).
- Gdy statystyki są świeże (analyze) i planer ocenia, że to będzie tańsze niż pełny skan.
Wszystko dzieje się automatycznie w planerze — niczego nie włączamy ręcznie. Zyskują na tym szczególnie zapytania analityczne/ad-hoc, które nie zawsze idealnie trafiają w istniejące indeksy.
Jak wykorzystać jeden indeks z prefiksem
Załóżmy, że mamy tabelę magazyn.towar ze statusem o niskiej krotności (cardinality) i znacznikiem modyfikacji.
-- utworzymy schemat, jeśli trzeba
create schema if not exists magazyn;
-- definicja tabeli:
-- - trzymamy czas w UTC jako timestamp (bez strefy)
-- - z niego wyprowadzamy datę w kolumnie STORED (immutable)
drop table if exists magazyn.towarx cascade;
create table magazyn.towarx (
towar_id bigint generated always as identity primary key,
nazwa text not null,
status text not null default 'aktywne', -- niska krotność
zmieniono_utc timestamp not null default (now() at time zone 'UTC'),
zmieniono_d date generated always as (date_trunc('day', zmieniono_utc)::date) stored
);
-- indeks pod skip scan: kolumna wiodąca o niskiej krotności + równość po dacie
create index concurrently if not exists ix_towarx_status_d
on magazyn.towarx (status, zmieniono_d);
Zapytanie pomijające kolumnę wiodącą – użyje skip scan
Chcemy policzyć krotki z danego dnia, ale nie filtrujemy po status (czyli: niezależnie od status):
explain analyze
select count(*)
from magazyn.towarx
where zmieniono_d = date '2025-01-01';
Plan w PostgreSQL 18 pokaże Index Scan / Index Only Scan używający ix_towar_status_d z warunkiem na zmieniono_d. Nie zobaczymy flagi „skip scan” wprost — to decyzja planera, ale fakt użycia indeksu z pominięciem kolumny wiodącej (status) oznacza, że zastosowano właśnie ten algorytm.
QUERY PLAN
----------------------------------------------------------------------------------------------------------------
Aggregate (cost=18.02..18.03 rows=1 width=8) (actual time=0.028..0.029 rows=1.00 loops=1)
Buffers: shared hit=1
-> Bitmap Heap Scan on towarx (cost=9.55..18.01 rows=4 width=0) (actual time=0.025..0.026 rows=0.00 loops=1)
Recheck Cond: (zmieniono_d = '2025-01-01'::date)
Buffers: shared hit=1
-> Bitmap Index Scan on ix_towarx_status_d (cost=0.00..9.55 rows=4 width=0) (actual time=0.007..0.008 rows=0.00 loops=1)
Index Cond: (zmieniono_d = '2025-01-01'::date)
Index Searches: 1
Buffers: shared hit=1
Planning:
Buffers: shared hit=2
Planning Time: 0.084 ms
Execution Time: 0.051 ms
(13 rows)
Gdzie magazyn.ix_towarx_status_d to:
\d magazyn.ix_towarx_status_d
Index "magazyn.ix_towarx_status_d"
Column | Type | Key? | Definition
-------------+------+------+-------------
status | text | yes | status
zmieniono_d | date | yes | zmieniono_d
btree, for table "magazyn.towarx"
Kontrast: sekwencyjny skan dla porównania
Aby przekonać się, że indeks naprawdę pomaga, możemy na chwilę wyłączyć skany indeksowe:
set enable_indexscan = off;
set enable_bitmapscan = off;
explain analyze
select count(*)
from magazyn.towarx
where zmieniono_d = date '2025-01-01';
Teraz planer powinien wybrać Seq Scan + Filter — zwykle droższy na większych tabelach:
explain analyze
select count(*)
from magazyn.towarx
where zmieniono_d = date '2025-01-01';
QUERY PLAN
---------------------------------------------------------------------------------
Aggregate (cost=19.01..19.02 rows=1 width=8) (actual time=0.008..0.009 rows=1.00 loops=1)
-> Seq Scan on towarx (cost=0.00..19.00 rows=4 width=0) (actual time=0.005..0.006 rows=0.00 loops=1)
Filter: (zmieniono_d = '2025-01-01'::date)
Planning:
Buffers: shared hit=2
Planning Time: 0.091 ms
Execution Time: 0.031 ms
Zapytanie „pełne” (z kolumną wiodącą) — klasyczny scenariusz
Oczywiście, gdy filtrujemy również po status, indeks (status, zmieniono_d) będzie użyty bez skip scan:
explain analyze
select *
from magazyn.towar
where status = 'aktywne'
and zmieniono_d = date '2025-01-01';
Planowany jako:
explain analyze
select *
from magazyn.towarx
where status = 'aktywne'
and zmieniono_d = date '2025-01-01';
QUERY PLAN
-----------------------------------------------------------------
Index Scan using ix_towarx_status_d on towarx (cost=0.15..8.17 rows=1 width=84) (actual time=0.009..0.010 rows=0.00 loops=1)
Index Cond: ((status = 'aktywne'::text) AND (zmieniono_d = '2025-01-01'::date))
Index Searches: 1
Buffers: shared hit=2
Planning:
Buffers: shared hit=104
Planning Time: 0.559 ms
Execution Time: 0.048 ms
(8 rows)
Dobre praktyki dla skip-scan
- Projektując indeks: umieszczamy kolumnę o niskiej krotności jako pierwszą (np. status), a kolumnę filtrowaną „równościowo” dalej (np. zmieniono_d).
- Równości po czasie: jeśli filtrujemy „po dacie”, a kolumna bazową jest timestamptz, należy rozważyć kolumnę date STORED (jak zmieniono_d) — ułatwia równościowe dopasowania i zwiększa szanse na skip scan.
- Statystyki: po większych zmianach danych uruchamiamy analyze (lub czekamy na autovacuum jeżeli był dla tabeli poprawnie skonfigurowany), by planer miał aktualny obraz krotności.
- Granice optymalizacji: skip scan nie wspiera, between — do zakresów należy rozważyć inne indeksy (np. osobny indeks po zmieniono_d albo indeks BRIN dla bardzo dużych, czasowo rosnących tabel).
Rozbudowana klauzula returning
PostgreSQL 18 wprowadza również istotne usprawnienie klauzuli returning, które zasadniczo zmienia sposób przechwytywania danych podczas operacji DML. Przed tym wydaniem returning miała ograniczenie: mogła zwracać tylko jeden zestaw wartości, zależnie od typu operacji.
Tradycyjne zachowanie było proste, ale ograniczające:
- insert i update: zwracały wyłącznie wartości nowe/bieżące,
- delete: zwracał wyłącznie wartości stare/usunięte,
- merge: zwracał wartości zależnie od konkretnej wewnętrznej operacji.
To ograniczenie często zmusza deweloperów do pisania oddzielnych zapytań, implementowania złożonych wyzwalaczy (triggers) lub stosowania obejść, gdy potrzebne było porównanie wartości „przed i po” albo pełne ujęcie informacji o zmianach.
PostgreSQL 18 eliminuje to ograniczenie, wprowadzając specjalne aliasy old i new, które pozwalają w sposób jawny odwoływać się zarówno do poprzedniego stanu, jak i do stanu bieżącego danych w ramach jednego zapytania DML. Ulepszenie działa dla wszystkich operacji DML: insert, update, delete i merge.
Zrozumienie rozbudowy klauzuli returning
Ulepszona klauzula returning udostępnia dwa specjalne aliasy, które stają się dostępne w poleceniach DML:
- old: odwołuje się do wartości krotki sprzed operacji,
- new: odwołuje się do wartości krotki po operacji.
Aliasów tych używa się nieco inaczej w zależności od typu wykonywanej operacji. Zrozumienie tego zachowania jest kluczowe dla skutecznego wykorzystania rozszerzonej klauzuli returning.
- Dla operacji insert wartości old są zazwyczaj null, ponieważ wcześniejsza krotka nie istniała, natomiast new zawiera dane wstawione. Istnieje jednak istotny wyjątek: przy insert ... on conflict do update alias old będzie zawierał dane istniejącej krotki, która spowodowała konflikt z wstawieniem.
- Dla operacji update alias old zawiera wartości krotki sprzed aktualizacji, a new — wartości po aktualizacji. To właśnie tutaj ulepszenie błyszczy najbardziej, ponieważ teraz można uchwycić oba stany w jednym poleceniu.
- Dla operacji delete alias old zawiera wartości krotki usuwanej, zaś new jest zazwyczaj null. Jednak jeśli operacja delete zostanie przekształcona w update przez regułę przepisywania (rewrite rule, np.: aktualizacja widoku), alias new może zawierać zaktualizowane dane krotki.
- Dla operacji merge zachowanie zależy od konkretnego działania (insert, update lub delete) i może obejmować wartości zarówno z tabeli źródłowej, jak i docelowej.
Rozbudowana klauzula returning – nowe możliwości
Dla pokazania rozbudowanej klauzuli returning zbudujmy prosty model gospodarki magazynowej.
-- schemat pomocniczy
create schema if not exists magazyn;
-- tabela towarów (zmienione nazwy i struktura względem oryginału)
create table magazyn.towar (
towar_id bigint generated always as identity primary key, -- klucz tożsamości
nazwa text not null,
kategoria text not null,
ilosc integer not null default 0,
cena_netto numeric(10,2) not null,
vat numeric(4,2) not null default 23.00, -- przykładowy vat %
cena_brutto numeric(10,2) generated always as (round(cena_netto * (1 + vat/100), 2)) stored,
zmieniono_at timestamptz default current_timestamp,
zmienil_uzytkownik text default current_user
);
-- dane startowe
insert into magazyn.towar (nazwa, kategoria, ilosc, cena_netto, vat) values
('laptop', 'elektronika', 50, 999.99, 23),
('krzesło biurowe', 'meble', 75, 249.50, 23),
('mysz bezprzew.', 'elektronika', 120, 29.95, 23),
('biurko stojące', 'meble', 30, 399.00, 23),
('kabel usb', 'elektronika', 200, 12.99, 23),
('fotel gamingowy', 'meble', 0, 89.99, 23);
Własne aliasy (with (old as ..., new as ...))
Zanim zaczniemy, warto zauważyć, że gdy chcemy bardziej opisowe lub niekolidujące nazwy aliasów: old/new, możemy je przypisać w zapytaniu (aliasy, do aliasów), stosując returning with (old as przed, new as po):
update magazyn.towar
set ilosc = ilosc * 2
where towar_id = 1
returning with (old as przed, new as po)
nazwa,
przed.ilosc as bylo,
po.ilosc as jest,
po.ilosc - przed.ilosc as przyrost;
nazwa | bylo | jest | przyrost
--------+------+------+----------
laptop | 60 | 120 | 60
(1 row)
Aliasami można też oddać domenę biznesową, np. podatki:
update magazyn.towar
set cena_netto = round(cena_netto * 1.23, 2) -- doliczenie 23% podatku
where kategoria = 'elektronika'
returning with (old as netto, new as opodatkowane)
nazwa,
netto.cena_netto as cena_bazowa,
opodatkowane.cena_netto as cena_po_podatku,
opodatkowane.cena_netto - netto.cena_netto as kwota_podatku;
nazwa | cena_bazowa | cena_po_podatku | kwota_podatku
----------------------+-------------+-----------------+---------------
kabel usb | 12.52 | 15.40 | 2.88
mysz bezprzew. | 28.87 | 35.51 | 6.64
klawiatura gamingowa | 86.75 | 106.70 | 19.95
laptop | 1060.29 | 1304.16 | 243.87
(4 rows)
UPDATE 4
insert + returning – podstawowe wykorzystanie nowej funkcjonalności
Kiedy wstawiamy nowe dane, często chcemy poznać wygenerowane wartości (id/pk, znaczniki czasu itp.).
-- proste insert z rozszerzonym returning
insert into magazyn.towar (nazwa, kategoria, ilosc, cena_netto, vat)
values ('klawiatura gamingowa', 'elektronika', 85, 89.99, 23)
returning
old.nazwa as nazwa_poprzednia, -- będzie null (brak starej krotki)
new.nazwa as nazwa_biezaca,
new.towar_id,
new.ilosc;
old.nazwa będzie null, a new.* zawiera wartości po wstawieniu.
nazwa_poprzednia | nazwa_biezaca | towar_id | ilosc
------------------+----------------------+----------+-------
␀ | klawiatura gamingowa | 7 | 85
(1 row)
insert ... on conflict: kiedy old ma znaczenie
Dodajemy unikalność nazwy, po czym spróbujmy wstawić duplikat i zwiększyć stan.
-- unikalność nazwy towaru
alter table magazyn.towar
add constraint uq_towar_nazwa unique (nazwa);
-- próba wstawienia towaru, który już istnieje: zwiększamy stan i aktualizujemy cenę
insert into magazyn.towar (nazwa, kategoria, ilosc, cena_netto, vat)
values ('laptop', 'elektronika', 25, 1100.00, 23)
on conflict (nazwa)
do update set
ilosc = magazyn.towar.ilosc + excluded.ilosc, -- dodajemy dostawę
cena_netto = excluded.cena_netto,
Asynchroniczne I/O
zmieniono_at = current_timestamp
returning
old.ilosc as ilosc_przed,
new.ilosc as ilosc_po,
old.cena_netto as cena_przed,
new.cena_netto as cena_po,
new.ilosc - old.ilosc as ilosc_dodana;
ilosc_przed | ilosc_po | cena_przed | cena_po | ilosc_dodana
-------------+----------+------------+---------+--------------
50 | 75 | 999.99 | 1100.00 | 25
(1 row)
old.* odnosi się do istniejącej krotki, która spowodowała konflikt.
update: najczęstszy przypadek użycia
Podnieśmy ceny netto w kategorii „elektronika” o 5% i pokażmy różnicę.
update magazyn.towar
set
cena_netto = round(cena_netto * 1.05, 2),
zmieniono_at = current_timestamp
where kategoria = 'elektronika'
returning
nazwa,
old.cena_netto as cena_przed,
new.cena_netto as cena_po,
round(new.cena_netto - old.cena_netto, 2) as wzrost;
nazwa | cena_przed | cena_po | wzrost
----------------------+------------+---------+--------
mysz bezprzew. | 29.95 | 31.45 | 1.50
kabel usb | 12.99 | 13.64 | 0.65
klawiatura gamingowa | 89.99 | 94.49 | 4.50
laptop | 1100.00 | 1155.00 | 55.00
(4 rows)
UPDATE 4
Przykład z logiką „biznesową” w returning — „wyprzedaż błyskawiczna”:
update magazyn.towar
set
ilosc = greatest(ilosc - 15, 0), -- nie schodź poniżej zera
cena_netto = round(cena_netto * 0.85, 2), -- rabat 15%
zmieniono_at = current_timestamp
where kategoria = 'elektronika' and ilosc > 20
returning
nazwa,
old.ilosc as stan_przed,
new.ilosc as stan_po,
old.cena_brutto as cena_brutto_regularna,
new.cena_brutto as cena_brutto_promocyjna,
case when old.ilosc - new.ilosc > 0 then old.ilosc - new.ilosc else 0 end as sztuki_sprzedane,
case when new.ilosc < 10 then 'NISKI_STAN' else 'OK' end as status_stanu;
Wynik:
nazwa | stan_przed | stan_po | cena_brutto_regularna | cena_brutto_promocyjna | sztuki_sprzedane | status_stanu
----------------------+------------+---------+-----------------------+------------------------+------------------+--------------
mysz bezprzew. | 120 | 105 | 38.68 | 32.88 | 15 | OK
kabel usb | 200 | 185 | 16.78 | 14.26 | 15 | OK
klawiatura gamingowa | 85 | 70 | 116.22 | 98.79 | 15 | OK
laptop | 75 | 60 | 1420.65 | 1207.55 | 15 | OK
(4 rows)
UPDATE 4
delete: co zostało usunięte – audyt / soft delete
Twarde usunięcie wybranych pozycji i zwrot informacji:
delete from magazyn.towar
where ilosc = 0 and kategoria = 'meble'
returning
old.towar_id,
old.nazwa,
old.cena_brutto as cena_koncowa,
old.zmieniono_at as ostatnia_aktywnosc,
'USUNIETE' as status,
current_timestamp as kiedy_usunieto;
towar_id | nazwa | cena_koncowa | ostatnia_aktywnosc | status | kiedy_usunieto
----------+-----------------+--------------+-------------------------------+----------+-------------------------------
6 | fotel gamingowy | 110.69 | 2025-09-22 08:06:01.276461+02 | USUNIETE | 2025-09-22 08:09:26.647626+02
(1 row)
DELETE 1
Wariant soft delete — dopiszemy status i zamiast delete użyjemy update:
-- kolumna statusu (jeśli jeszcze nie ma)
alter table magazyn.towar
add column if not exists status text default 'aktywne';
-- „soft delete” z informacją o zmianie
update magazyn.towar
set
status = 'wycofane',
zmieniono_at = current_timestamp
where ilosc < 150 and kategoria = 'elektronika'
returning
nazwa,
old.status as status_przed,
new.status as status_po,
old.ilosc as ilosc_przed,
extract(day from (current_timestamp - old.zmieniono_at)) as dni_od_ostatniej_zmiany;
nazwa | status_przed | status_po | ilosc_przed | dni_od_ostatniej_zmiany
----------------------+--------------+-----------+-------------+-------------------------
mysz bezprzew. | aktywne | wycofane | 105 | 0
klawiatura gamingowa | aktywne | wycofane | 70 | 0
laptop | aktywne | wycofane | 60 | 0
(3 rows)
rewrite rule — „soft delete” + returning old/new
Każde delete na magazyn.towar ma zostać przepisane na update zmieniający status na „wycofane”, z pełnym audytem tego, co się zmieniło.
-- przygotujemy kolejną krotkę testową:
insert into magazyn.towar (nazwa, kategoria, ilosc, cena_netto, vat)
values ('klawiatura numeryczna', 'elektronika', 10, 49.90, 23)
on conflict (nazwa) do nothing;
-- usuwamy starą regułę, jeśli istnieje
drop rule if exists towar_soft_delete on magazyn.towar;
-- reguła: delete -> update (soft delete) Z RETURNING typu krotki tabeli
create rule towar_soft_delete as
on delete to magazyn.towar
do instead
update magazyn.towar t
set status = 'wycofane',
zmieniono_at = clock_timestamp()
where t.towar_id = old.towar_id
returning t.*; -- WAŻNE: zwracamy dokładnie rowtype tabeli magazyn.towar
Użycie i efekt (audyt w jednym zapytaniu):
-- audyt w jednym zapytaniu: własna projekcja z old/new
delete from magazyn.towar
where nazwa = 'klawiatura numeryczna'
returning
old.towar_id,
old.nazwa,
old.status as status_przed,
new.status as status_po,
old.zmieniono_at as ts_przed,
new.zmieniono_at as ts_po;
Wynik:
towar_id | nazwa | status_przed | status_po | ts_przed | ts_po
----------+-----------------------+--------------+-----------+-------------------------------+-------------------------------
9 | klawiatura numeryczna | wycofane | wycofane | 2025-09-22 08:29:39.712562+02 | 2025-09-22 08:31:44.764646+02
(1 row)
DELETE 0
Jak to zadziało?
- Parser przyjmie delete, rewrite przepisze je na update (soft delete).
- old.* pokaże stan sprzed (status = aktywne), new.* — stan po (wycofane).
- Realnie żadnego fizycznego usunięcia nie będzie, ale semantyka „usunięcia” jest zachowana biznesowo.
Uwaga
- Reguły działają przed planowaniem wykonania zapytania i zamieniają całe zapytanie; do efektów „per krotka” częściej używamy wyzwalaczy(triggers). W naszym przypadku reguła jest idealna, bo globalnie przekierowuje delete na update.
merge: scalanie w nowej odsłonie returning
Mamy tabelę magazyn.dostawa (źródło), którą scalamy z magazyn.towar.
- Jeżeli towar istnieje: zwiększamy ilość (liczbę!) i aktualizujemy cenę netto.
- Jeżeli nie istnieje: wstawiamy nową krotkę.
Dzięki returning od razu widzimy, co i jak zostało zmienione.
-- usuwamy, jeżeli utworzona była w poprzednim przykładzie starą regułę, jeśli istnieje
drop rule if exists towar_soft_delete on magazyn.towar;
-- tabela dostaw (źródło merge) – celowo inna struktura
drop table if exists magazyn.dostawa cascade;
create table magazyn.dostawa (
dostawa_id bigint generated always as identity primary key,
nazwa_towaru text not null,
kategoria text not null,
ilosc_dostarczona integer not null,
nowa_cena_netto numeric(10,2),
vat numeric(4,2) default 23.00
);
-- próbka dostaw: jedna nowa pozycja, jedna istniejąca, jedna z aktualizacją ceny
insert into magazyn.dostawa (nazwa_towaru, kategoria, ilosc_dostarczona, nowa_cena_netto, vat) values
('laptop', 'elektronika', 15, 1149.00, 23), -- istnieje: zwiększymy ilosc + cenę
('mysz bezprzew.', 'elektronika', 40, 27.90, 23), -- istnieje: zwiększymy ilosc + zmienimy cenę
('monitor 27 cali', 'elektronika', 20, 799.00, 23); -- nowy: wstawimy
MERGE z audytem zmian:
-- ważne: na docelowej tabeli powinna być jednoznaczna identyfikacja rekordu biznesowego,
-- tu przyjmijmy unikalność po 'nazwa': ograniczenie/unikalny indeks – zadziała z IF NOT EXISTS i może być CONCURRENTLY
create unique index concurrently if not exists ix_towar_nazwa
on magazyn.towar (nazwa);
-- scalanie: źródło (d) -> cel (t) -> MERGE: aktualizuj istniejące / wstaw nowe + audyt w RETURNING
merge into magazyn.towar t
using (
select nazwa_towaru, kategoria, ilosc_dostarczona, nowa_cena_netto, vat
from magazyn.dostawa
) as d
on (t.nazwa = d.nazwa_towaru)
when matched then
update set
ilosc = t.ilosc + d.ilosc_dostarczona,
cena_netto = coalesce(d.nowa_cena_netto, t.cena_netto),
vat = coalesce(d.vat, t.vat),
zmieniono_at = clock_timestamp()
when not matched then
insert (nazwa, kategoria, ilosc, cena_netto, vat, zmieniono_at)
values (d.nazwa_towaru, d.kategoria, d.ilosc_dostarczona,
coalesce(d.nowa_cena_netto, 0), coalesce(d.vat, 23), clock_timestamp())
returning with (old as przed, new as po)
case when przed.nazwa is null then 'insert' else 'update' end as akcja,
coalesce(przed.nazwa, po.nazwa) as nazwa_towaru,
przed.ilosc as ilosc_przed,
po.ilosc as ilosc_po,
przed.cena_netto as cena_przed,
po.cena_netto as cena_po,
po.ilosc - coalesce(przed.ilosc, 0) as roznica_ilosci;
Wynik:
akcja | nazwa_towaru | ilosc_przed | ilosc_po | cena_przed | cena_po | roznica_ilosci
--------+-----------------+-------------+----------+------------+---------+----------------
update | laptop | 120 | 135 | 1304.16 | 1149.00 | 15
update | mysz bezprzew. | 105 | 145 | 35.51 | 27.90 | 40
insert | monitor 27 cali | ␀ | 20 | ␀ | 799.00 | 20
(3 rows)
MERGE 3
Co widzimy w wynikach returning:
- dla istniejących pozycji (matched → update): akcja = 'UPDATE’, wypełnione przed.* i po.*, różnica ilości = dostawa, zmieniona cena (jeśli podano);
- dla nowych (not matched → insert): akcja = 'INSERT’, przed.* będzie null, po.* pokaże świeżo wstawioną krotkę.
Kontrola po scaleniu
-- weryfikacja wyników w tabeli docelowej
select nazwa, kategoria, ilosc, cena_netto, cena_brutto, status, zmieniono_at
from magazyn.towar
where nazwa in ('laptop','mysz bezprzew.','monitor 27 cali')
order by nazwa;
Wynik:
nazwa | kategoria | ilosc | cena_netto | cena_brutto | status | zmieniono_at
-----------------+-------------+-------+------------+-------------+----------+-------------------------------
laptop | elektronika | 135 | 1149.00 | 1413.27 | wycofane | 2025-09-22 08:43:19.893947+02
monitor 27 cali | elektronika | 20 | 799.00 | 982.77 | aktywne | 2025-09-22 08:43:19.894535+02
mysz bezprzew. | elektronika | 145 | 27.90 | 34.32 | wycofane | 2025-09-22 08:43:19.894514+02
(3 rows)
Wydajność: na co należy uważać z nową wersją returning
Rozszerzone returning oznacza, że silnik dla zwracanych wyników musi utrzymać jednocześnie stare i nowe wersje krotek w pamięci. Przy dużych partiach aktualizacji/usuwania należy wykonywać proces w paczkach i indeksować kolumny używane w filtrze: WHERE.
Przykład paczkowania:
-- przetwarzaj partiami po kluczach (tu: 1..1000)
update magazyn.towar
set cena_netto = round(cena_netto * 1.05, 2)
where towar_id between 1 and 1000
returning old.cena_netto, new.cena_netto;
Przykład indeksu wspierającego typowy wzorzec:
create index if not exists ix_towar_kategoria_ilosc
on magazyn.towar (kategoria, ilosc);
explain update magazyn.towar
set ilosc = ilosc + 10
where kategoria = 'elektronika' and ilosc Index Scan using ix_towar_kategoria_ilosc on towar (cost=0.13..8.20 rows=3 width=10)
Index Cond: ((kategoria = 'elektronika'::text) AND (ilosc < 150))
(3 rows)
Dobre praktyki
- Zwracanie tylko potrzebnych kolumn (np. old.ilosc, new.ilosc, new.cena_netto) zamiast old.* , new.* przy dużych tabelach/ zapytaniach.
- Dodawanie wyliczenia biznesowego bezpośrednio w returning (statusy, progi, różnice).
- Zwracanie szczególnej uwagi na null (np. coalesce(old.ilosc, 0)), zwłaszcza przy insert i delete.
Kolumny generowane „w locie”
PostgreSQL 18 wprowadza wirtualne kolumny generowane. Wcześniej kolumny generowane były zawsze przechowywane na dysku. Oznaczało to, że dla takich kolumn wartości obliczano w czasie insert lub update, dokładano więc nieco „kosztu” zapisu.
W PostgreSQL 18 domyślnym typem kolumn generowanych są teraz kolumny wirtualne. Jeśli zdefiniujemy kolumnę generowaną bez jawnego stored, zostanie utworzona jako kolumna wirtualna.
create table magazyn.user_profiles (
user_id serial primary key,
settings jsonb,
username varchar(100) generated always as (settings ->> 'username') virtual
);
To świetna aktualizacja dla osób korzystających z JSON — zapytania można uprościć, a zmiany danych lub normalizację wykonywać w locie, gdy jest taka potrzeba.
Należy zauważyć, że kolumn wirtualnych nie można indeksować — ponieważ nie są przechowywane na dysku. Aby indeksować jsonb, należy użyć wersji stored lub indeksu na wyrażeniu.
Wirtualne kolumny generowane w magazynie — ekspresja bez zapisu
Wirtualne kolumny w istniejącej tabeli magazyn.towar
Dodajemy pochodną kolumnę „slug” (do linków/SEO) oraz upraszczamy wyciąganie marki z nazwy — bez zapisu na dysk.
-- 1) „slug” z nazwy towaru (tylko do odczytu – VIRTUAL domyślnie w PG18)
alter table magazyn.towar
add column if not exists nazwa_slug text
generated always as (
regexp_replace(lower(nazwa), '[^a-z0-9]+', '-', 'g')
);
-- 2) marka wyciągnięta z nazwy (np. pierwszy token przed spacją)
alter table magazyn.towar
add column if not exists marka text
generated always as (
split_part(nazwa, ' ', 1)
);
-- Uwaga: kolumny VIRTUAL nie są przechowywane na dysku i nie można ich bezpośrednio indeksować.
-- Jeśli potrzebujemy wyszukiwania po „slug”, tworzymy indeks na WYRAŻENIU:
create index if not exists ix_towar_slug_expr
on magazyn.towar ( (regexp_replace(lower(nazwa), '[^a-z0-9]+', '-', 'g')) );
-- Analogicznie dla „marka” – indeks na wyrażeniu:
create index if not exists ix_towar_marka_expr
on magazyn.towar ( (split_part(nazwa, ' ', 1)) );
Wirtualne kolumny w nowej tabeli magazyn.ruch_magazynowy
Rejestrujemy ruchy magazynowe (przyjęcia/rozchody) w kolumnie dane (jsonb), a jednocześnie chcemy mieć wygodne, niezmaterializowane widoki pól i wyliczeń. Dla najczęściej filtrowanego pola (np. wartość brutto) pokazujemy również wariant STORED.
drop table if exists magazyn.ruch_magazynowy cascade;
create table magazyn.ruch_magazynowy (
ruch_id bigint generated always as identity primary key,
towar_id bigint not null references magazyn.towar(towar_id),
typ_ruchu text not null check (typ_ruchu in ('PZ','WZ','ZW','MM')), -- przyjęcie/wydanie/zwrot/między-mag.
dane jsonb not null, -- np. {"ilosc": 5, "cena_netto": 89.99, "vat": 23, "nazwa": "mysz bezprzew."}
utworzono_at timestamptz default clock_timestamp(),
-- WIRTUALNE: podgląd atrybutów z JSONB (bez zapisu na dysk)
nazwa_towaru text
generated always as (dane ->> 'nazwa'),
ilosc integer
generated always as ((dane ->> 'ilosc')::integer),
cena_netto numeric(10,2)
generated always as ((dane ->> 'cena_netto')::numeric),
vat numeric(4,2)
generated always as (coalesce((dane ->> 'vat')::numeric, 23.00)),
wartosc_netto numeric(12,2)
generated always as (round(((dane ->> 'cena_netto')::numeric) * ((dane ->> 'ilosc')::integer), 2)),
-- STORED: gdy często filtrujemy po wartości brutto – materializujemy i możemy łatwo indeksować
wartosc_brutto numeric(12,2)
generated always as (round(((dane ->> 'cena_netto')::numeric) * (1 + coalesce((dane ->> 'vat')::numeric, 23.00)/100)
* ((dane ->> 'ilosc')::integer), 2)) stored
);
-- Indeksowanie:
-- 1) na kolumnie STORED – zwykły indeks
create index if not exists ix_ruch_brutto on magazyn.ruch_magazynowy (wartosc_brutto);
-- 2) na kolumnach VIRTUAL – robimy indeks NA WYRAŻENIU odwzorowującym definicję:
create index if not exists ix_ruch_nazwa_expr
on magazyn.ruch_magazynowy ( (dane ->> 'nazwa') );
create index if not exists ix_ruch_wartosc_netto_expr
on magazyn.ruch_magazynowy ( (round(((dane ->> 'cena_netto')::numeric) * ((dane ->> 'ilosc')::integer), 2)) );
Użycie (przykład):
-- przykładowy wpis PZ (przyjęcie zewnętrzne)
insert into magazyn.ruch_magazynowy (towar_id, typ_ruchu, dane)
values (
(select towar_id from magazyn.towar where nazwa = 'mysz bezprzew.' limit 1),
'PZ',jsonb_build_object('nazwa','mysz bezprzew.','ilosc',40,'cena_netto',27.90,'vat',23)
)
returning ruch_id, nazwa_towaru, ilosc, cena_netto, wartosc_netto, wartosc_brutto;
Najważniejsze wskazówki
- Kolumny generowane domyślnie są wirtualne (obliczane przy odczycie).
- Indeksowanie: wirtualnych kolumn nie indeksujemy bezpośrednio — używamy indeksów na wyrażeniu; jeśli to niewygodne lub często używane, wybierz stored.
- Koszt: virtual → tańszy zapis, droższy odczyt (liczenie „w locie”); stored → droższy zapis, szybszy odczyt/filtrowanie.
- W czasie insert/update nie generujemy dla tych kolumn WAL — co może mieć znaczenie przy replikacji fizycznej.
OAuth 2.0
PostgreSQL 18 ma również dobre wiadomości dla deweloperów używających Okta, Keycloak i innych zarządzanych usług uwierzytelniania — silnik w wersji 18 jest teraz zgodny z OAuth 2.0. OAuth 2.0 konfigurujemy w głównym pliku uwierzytelniania połączeń (pg_hba.conf).
System OAuth używa tokenów bearer, gdzie aplikacja-klient przedstawia token zamiast hasła, by potwierdzić tożsamość. Token jest nieprzezroczystym ciągiem, a jego format zależy od serwera autoryzacji. Funkcja ta eliminuje potrzebę przechowywania haseł w bazie. Pozwala również, aby bardziej zaawansowane środki bezpieczeństwa, jak MFA i SSO, były zarządzane przez zewnętrznych dostawców tożsamości.
OAuth 2.0 w pg_hba.conf — logowanie tokenem
Minimalny zarys konfiguracji:
# pg_hba.conf
# klient łączy się tokenem bearer zamiast hasła
# wymagane: build z obsługą OAuth klienta oraz moduł validatora
host all all 0.0.0.0/0 oauth issuer=https://auth.example.com \
scope="openid profile" \
validator=oidc
Parametry issuer, scope i opcjonalnie validator są zależne od dostawcy (np. Keycloak/Okta). Funkcjonalność wymaga włączenia klienta OAuth przy budowie silnika oraz skonfigurowania modułu weryfikatora tokenów. Więcej ciekawych informacji na temat wdrożenia OAuth, możemy znaleźć na:
- Preview PostgreSQL 18’s OAuth2 Authentication (1) – Explore How it Works
- Preview PostgreSQL 18’s OAuth2 Authentication (2) – Building a Custom OAuth2 Validator by Rust
- Preview PostgreSQL 18’s OAuth2 Authentication (3) – Enhancing a PostgreSQL Client Library to Speak OAUTHBEARER
Podsumowanie
PostgreSQL w wersji 18 zawiera kilka tysięcy commitów, pochodzących od ponad 200 autorów. Chociaż wiele z nich stanowią funkcje, w nowej wersji znalazło się mnóstwo dodatków i optymalizacji samego silnika, w szczególności w planerze zapytań. Nawet jeżeli nie korzystamy z funkcji opcjonalnych, wciąż zyskamy na: wydajności, poprawkach błędów i łatkach bezpieczeństwa — co czyni regularne aktualizacje silnika dobrym pomysłem.
Szczegółowe informacje o nowościach omówione zostały także tutaj: PostgreSQL.
