Tłumaczenie zapytania między dialektami: przykład krok po kroku (PostgreSQL → SQL Server)
Przetłumacz zapytanie z PostgreSQL na SQL Server linijka po linijce: LIMIT/OFFSET, || konkatenacja, ILIKE, rzutowania ::, boolean, date_trunc, RETURNING.
Przeniesienie zapytania z PostgreSQL do SQL Servera rzadko bywa czystym kopiuj-wklej. W rdzeniu oba mówią tym samym standardem SQL, ale gdy tylko sięgniesz po paginację, funkcje tekstowe, rzutowania czy wartości logiczne, każdy silnik upiera się przy własnej pisowni. Ten artykuł bierze jedno realistyczne zapytanie PostgreSQL i tłumaczy je na SQL Server różnica po różnicy, a każdą regułę sprawdza w oficjalnej dokumentacji PostgreSQL i Microsoftu. Chodzi o metodę, którą da się powtórzyć, a nie o ściągę, której trzeba ślepo ufać.
Jak wygląda oryginalne zapytanie PostgreSQL?
Oto zapytanie raportujące z gatunku tych, które naprawdę spotykasz — stronicowana lista opłaconych zamówień dla jednej domeny klienta, z sformatowanym nazwiskiem, rzutowaną kwotą i miesiącem zamówienia:
SELECT
o.id::text AS order_ref,
c.first_name || ' ' || c.last_name AS customer,
o.total::numeric(10,2) AS amount,
date_trunc('month', o.created_at) AS order_month,
o.is_paid
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE c.email ILIKE '%@acme.com'
AND o.is_paid = true
ORDER BY o.created_at DESC
LIMIT 20 OFFSET 40;
Każda wyróżniona konstrukcja — ::, ||, date_trunc, ILIKE, true, LIMIT/OFFSET — to miejsce, w którym bezpośrednie wklejenie do SQL Servera się wywali. Weźmy je po kolei.
Jak przetłumaczyć LIMIT/OFFSET na SQL Server?
PostgreSQL stronicuje przez LIMIT count i OFFSET start (PostgreSQL: LIMIT and OFFSET). SQL Server nie ma LIMIT. Jego historycznym narzędziem jest TOP (expression), które ogranicza liczbę wierszy, ale nie ma własnego offsetu (Microsoft Learn: TOP) — więc samo TOP nie wyrazi „pomiń 40, weź 20”.
Odpowiedzią przenośną jest standardowa klauzula OFFSET ... FETCH, którą SQL Server obsługuje przy ORDER BY:
ORDER BY o.created_at DESC
OFFSET 40 ROWS FETCH NEXT 20 ROWS ONLY;
Zwróć uwagę, że kolejność się odwraca: PostgreSQL pisze LIMIT 20 OFFSET 40, a SQL Server OFFSET 40 ... FETCH NEXT 20. I OFFSET ... FETCH jest poprawne tylko z ORDER BY (Microsoft Learn: klauzula ORDER BY) — co jest zaletą, nie ograniczeniem: paginacja bez deterministycznego sortowania to błąd na obu silnikach.
Jak zamienić konkatenację ||?
W PostgreSQL || skleja łańcuchy — 'Post' || 'greSQL' zwraca PostgreSQL (PostgreSQL: funkcje tekstowe). SQL Server nie używa do tego ||; jego operatorem konkatenacji jest +, jak w SELECT 'book' + 'case', które zwraca bookcase (Microsoft Learn: konkatenacja łańcuchów +). Wyrażenie z nazwiskiem zmienia się więc w:
c.first_name + ' ' + c.last_name AS customer
Czai się tu pułapka semantyczna, którą warto nazwać. SQL Server oferuje też funkcję CONCAT i kusi, by po nią sięgnąć — ale CONCAT niejawnie zamienia wartości NULL na puste łańcuchy (Microsoft Learn: CONCAT), podczas gdy + (tak jak postgresowe ||) propaguje NULL: sklejenie czegokolwiek z NULL daje NULL. Jeśli last_name bywa puste, + wiernie odtwarza zachowanie PostgreSQL, a CONCAT po cichu zmienia wynik. Wybieraj to, co pasuje do przenoszonej semantyki, a nie to, co ładniej wygląda.
Czym zastąpić ILIKE dla dopasowania bez rozróżniania wielkości liter?
ILIKE to postgresowy LIKE ignorujący wielkość liter. Dokumentacja mówi wprost, że „można go użyć zamiast LIKE, by dopasowanie było niewrażliwe na wielkość liter”, że „nie jest w standardzie SQL, lecz jest rozszerzeniem PostgreSQL”, a odpowiada mu operator ~~* (PostgreSQL: dopasowywanie wzorców). SQL Server w ogóle nie ma słowa kluczowego ILIKE.
W SQL Serverze o wrażliwości LIKE na wielkość liter decyduje collation (ustawienie porównywania), a nie operator. Wiele baz działa na domyślnym collation niewrażliwym na wielkość liter, w którym zwykłe LIKE już ją ignoruje — ale nie wolno tego zakładać. Żeby wymusić dopasowanie niewrażliwe niezależnie od collation kolumny, albo sprowadź obie strony przez LOWER (Microsoft Learn: LOWER), albo nałóż na porównanie jawne collation niewrażliwe (_CI_) klauzulą COLLATE (Microsoft Learn: collations):
-- przenośne, działa przy dowolnym collation kolumny:
WHERE LOWER(c.email) LIKE '%@acme.com'
-- albo z jawnie wymuszonym collation:
WHERE c.email COLLATE Latin1_General_100_CI_AS LIKE '%@acme.com'
LOWER po obu stronach to najbardziej przewidywalny wybór, gdy portujesz i nie panujesz nad collation celu. Pamiętaj tylko, że — jak każda funkcja opakowująca kolumnę — może uniemożliwić użycie indeksu.
Jak przepisać rzutowania :: i wartości logiczne?
PostgreSQL przyjmuje dwie składnie rzutowania: standardową CAST(expression AS type) oraz skrót „historycznego użycia PostgreSQL” expression::type (PostgreSQL: rzutowania typów). SQL Server wspiera wyłącznie CAST(expression AS data_type) (albo CONVERT) (Microsoft Learn: CAST i CONVERT). Oba rzutowania :: rozwijają się więc do formy jawnej:
CAST(o.id AS varchar(20)) AS order_ref,
CAST(o.total AS decimal(10,2)) AS amount,
Większą zmianą jest wartość logiczna. PostgreSQL ma prawdziwy typ boolean, który przyjmuje literały takie jak TRUE, FALSE i NULL (PostgreSQL: typ boolean). SQL Server nie ma typu logicznego; odpowiednikiem jest bit, „całkowity typ danych przyjmujący wartość 1, 0 lub NULL”, w którym łańcuch TRUE konwertuje się na 1, a FALSE na 0 (Microsoft Learn: bit). Zatem o.is_paid = true staje się:
AND o.is_paid = 1
a sama kolumna is_paid w schemacie SQL Servera jest zadeklarowana jako bit, a nie boolean.
A co z date_trunc?
Postgresowe date_trunc('month', o.created_at) obcina znacznik czasu do zadanej precyzji — date_trunc('hour', timestamp '2001-02-16 20:38:40') zwraca 2001-02-16 20:00:00 (PostgreSQL: funkcje daty/czasu). SQL Server 2022 (16.x) wprowadził bezpośredni odpowiednik, DATETRUNC(datepart, date) (Microsoft Learn: DATETRUNC):
DATETRUNC(month, o.created_at) AS order_month -- SQL Server 2022+
Przed 2022 nie ma DATETRUNC, a utrwalonym idiomem jest przejście tam i z powrotem przez datę bazową zero za pomocą DATEADD/DATEDIFF (Microsoft Learn: DATEADD):
DATEADD(month, DATEDIFF(month, 0, o.created_at), 0) AS order_month
Za pierwszym razem czyta się to nieprzejrzyście, ale to poprawny odpowiednik sprzed 2022: policz pełne miesiące od daty bazowej, a potem dodaj je z powrotem.
Jak przenieść RETURNING i generate_series?
Dwie kolejne konstrukcje PostgreSQL pojawiają się bez przerwy i nie mają dosłownej pisowni w SQL Serverze.
RETURNING pozwala postgresowym INSERT/UPDATE/DELETE oddać wiersze, które zmieniły (PostgreSQL: RETURNING). Odpowiednikiem w SQL Serverze jest klauzula OUTPUT, która „zwraca informacje z każdego wiersza objętego operacją lub wyrażenia na nim oparte” i udostępnia pseudotabele INSERTED oraz DELETED (Microsoft Learn: OUTPUT):
-- PostgreSQL
INSERT INTO orders (customer_id, total) VALUES (42, 99.90) RETURNING id;
-- SQL Server
INSERT INTO orders (customer_id, total) OUTPUT INSERTED.id VALUES (42, 99.90);
generate_series produkuje zbiór wartości — generate_series(1, 10) daje liczby całkowite od 1 do 10 (PostgreSQL: funkcje zwracające zbiór). SQL Server 2022 dodał funkcję o tej samej nazwie, GENERATE_SERIES(start, stop [, step]), która wymaga poziomu zgodności 160 (Microsoft Learn: GENERATE_SERIES). Na starszych wersjach przenośnym zamiennikiem jest rekurencyjne wyrażenie CTE:
WITH series AS (
SELECT 1 AS n
UNION ALL
SELECT n + 1 FROM series WHERE n < 10
)
SELECT n FROM series OPTION (MAXRECURSION 100);
Jak wygląda w pełni przetłumaczone zapytanie?
Złóż wszystkie reguły razem, a wersja wyjściowego zapytania dla SQL Servera wygląda tak:
SELECT
CAST(o.id AS varchar(20)) AS order_ref,
c.first_name + ' ' + c.last_name AS customer,
CAST(o.total AS decimal(10,2)) AS amount,
DATETRUNC(month, o.created_at) AS order_month, -- SQL Server 2022+
o.is_paid
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE LOWER(c.email) LIKE '%@acme.com'
AND o.is_paid = 1
ORDER BY o.created_at DESC
OFFSET 40 ROWS FETCH NEXT 20 ROWS ONLY;
Sześć konstrukcji, sześć odrębnych reguł — a to tylko jedno zapytanie. Ten sam podział wychodzi wszędzie, gdzie przenosisz się między silnikami, i dlatego praca międzysilnikowa mniej dotyczy standardu SQL, a bardziej tych ostatnich 10%. To dokładnie różnice dialektów, które cię zaskoczą w pięciu silnikach, a rozbieżność nie kończy się na składni: nawet czytanie planu wykonania używa innych poleceń i słów kluczowych na każdym silniku.
Ujawnienie: ten blog wydaje LumaSoft (LUMA sp. z o.o.), a ja pracuję przy opisanym tu narzędziu.
SQL Sage for DBeaver to plugin, który dodaje AI copilota wewnątrz DBeavera i jest świadomy dialektu pięciu silników — PostgreSQL, MySQL/MariaDB, Oracle, SQL Server i SQLite — więc gdy poprosisz go o przepisanie zapytania na inny silnik, paginacja, rzutowania, funkcje tekstowe i obsługa dat pasują do połączenia, na którym pracujesz. Kilka faktów wprost: jest wyłącznie dla Windows (Linux i macOS nie są wspierane), jest keyless (logujesz się własnym kontem Claude lub ChatGPT, bez klucza API) i działa read-only domyślnie — na PostgreSQL oraz MySQL/MariaDB ten tryb tylko-do-odczytu jest wymuszany na poziomie silnika. Oba pluginy zobaczysz na stronie produktów. Nie jest powiązany z Microsoft, PostgreSQL ani DBeaver.
Trwały wniosek to metoda, a nie mapowanie: tłumacz jedną konstrukcję naraz, każdą potwierdzaj w dokumentacji silnika docelowego i wypatruj pułapek semantycznych — CONCAT kontra + przy nullach, collation kontra ILIKE, DATETRUNC kontra idiom DATEADD — bo to właśnie tam zapytanie, które „się uruchamia”, po cichu zwraca błędny wynik.