Jeden AI copilot dla pięciu silników baz danych: różnice dialektów, które cię zaskoczą (i jak sobie z nimi radzić)

Paginacja, cytowanie identyfikatorów i plany zapytań różnią się między PostgreSQL, MySQL/MariaDB, Oracle, SQL Server i SQLite. Praktyczny przewodnik z SQL.

Jeśli pracujesz z więcej niż jedną bazą danych, znasz to uczucie: zapytanie, które bez zarzutu działało na PostgreSQL, zwraca błąd składni w momencie, gdy wklejasz je do SQL Servera. Standard SQL istnieje, ale każdy silnik dokłada do niego własny dialekt. Różnice są na tyle drobne, że wydają się błahe, i na tyle ostre, że potrafią kosztować całe popołudnie.

Ten artykuł przechodzi przez różnice dialektów, które najczęściej zaskakują przy przełączaniu się między PostgreSQL, MySQL/MariaDB, Oracle, SQL Server i SQLite — z gotowym do skopiowania SQL dla każdego z nich. Jest przydatny niezależnie od tego, czy kiedykolwiek sięgniesz po konkretne narzędzie; chodzi o to, żeby te pułapki przestały cię zaskakiwać.

Jak stronicować wyniki: LIMIT vs TOP vs ROWNUM vs FETCH?

Paginacja to rzecz, którą najczęściej robi się źle przy przenoszeniu zapytań między silnikami, bo istnieją tu naprawdę trzy różne rodziny składni.

PostgreSQL, MySQL/MariaDB i SQLite używają LIMIT / OFFSET:

SELECT id, title
FROM articles
ORDER BY published_at DESC
LIMIT 10 OFFSET 20;

MySQL i MariaDB akceptują też skrót — LIMIT 20, 10 — gdzie najpierw jest offset, a potem liczba wierszy. Ta odwrócona kolejność to klasyczny błąd przy portowaniu zapytań, więc trzymaj się jawnej formy LIMIT ... OFFSET ....

SQL Server tradycyjnie używa TOP, które nie ma własnego offsetu:

SELECT TOP (10) id, title
FROM articles
ORDER BY published_at DESC;

Standardowa klauzula SQL OFFSET ... FETCH to opcja przenośna — obsługują ją dziś SQL Server (od 2012), Oracle (od 12c) i PostgreSQL. Wymaga ORDER BY:

SELECT id, title
FROM articles
ORDER BY published_at DESC
OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;

Oracle przed 12c nie miał ani LIMIT, ani klauzuli FETCH, więc stronicowało się przez ROWNUM wewnątrz zagnieżdżonego podzapytania — bo ROWNUM jest przypisywany przed zastosowaniem ORDER BY:

SELECT * FROM (
  SELECT a.*, ROWNUM rn FROM (
    SELECT id, title FROM articles ORDER BY published_at DESC
  ) a WHERE ROWNUM <= 30
) WHERE rn > 20;

Ten wzorzec wciąż spotkasz w starszych kodach Oracle, więc warto go rozpoznawać. Od 12c preferuj OFFSET ... FETCH (Oracle: row limiting clause, 12c).

Jedna zasada obowiązuje wszędzie: paginacja bez deterministycznego ORDER BY to błąd. Bez pełnego uporządkowania silnik może zwrócić wiersze w dowolnej kolejności, więc strona 2 potrafi powtórzyć albo pominąć wiersze ze strony 1.

Jak cytować identyfikatory w różnych silnikach?

Gdy nazwa kolumny lub tabeli koliduje ze słowem zarezerwowanym (user, order, group) albo zawiera spację, trzeba ją ocytować — a znak cytowania nie jest wszędzie taki sam.

-- PostgreSQL, Oracle, SQLite, standardowy SQL: cudzysłowy
SELECT "user" FROM "Orders";

-- MySQL / MariaDB (tryb domyślny): grawisy (backticki)
SELECT `user` FROM `Orders`;

-- SQL Server: nawiasy kwadratowe
SELECT [user] FROM [Orders];

Czają się tu dwie pułapki.

Zwijanie wielkości liter w nieocytowanych nazwach. PostgreSQL sprowadza nieocytowane identyfikatory do małych liter; Oracle do WIELKICH LITER. Czyli SELECT MyCol FROM t w rzeczywistości odpytuje mycol w PostgreSQL i MYCOL w Oracle. W chwili, gdy opakujesz nazwę w cudzysłowy, dokładna wielkość liter zostaje zachowana — co oznacza, że "MyCol" i nieocytowane MyCol mogą wskazywać na dwie różne kolumny. Dlatego tabela utworzona z ocytowanymi nazwami o mieszanej wielkości liter jest potem tak uciążliwa w odpytywaniu.

MySQL a cudzysłowy. Domyślnie MySQL i MariaDB traktują "user" jako literał tekstowy, a nie identyfikator. Cudzysłowy cytują identyfikatory dopiero po włączeniu ANSI_QUOTES w sql_mode. Grawisy działają zawsze, dlatego narzędzia dla MySQL opierają się na nich.

SQLite jest tu pragmatycznym wyjątkiem: akceptuje cudzysłowy, nawiasy kwadratowe oraz grawisy jako cytowanie identyfikatorów — właśnie dla zgodności z pozostałymi silnikami (SQLite: keywords & quoting).

Jak odczytać plan wykonania zapytania w każdym silniku?

Gdy zapytanie jest wolne, każdy silnik potrafi pokazać, jak zamierza je wykonać — ale komenda się różni, podobnie jak to, czy zapytanie zostaje faktycznie uruchomione.

-- PostgreSQL: sam szacunek albo faktyczne uruchomienie z pomiarem
EXPLAIN SELECT ...;
EXPLAIN ANALYZE SELECT ...;

-- MySQL 8.0.18+: EXPLAIN ANALYZE uruchamia zapytanie i raportuje realne czasy
EXPLAIN ANALYZE SELECT ...;

-- MariaDB: ANALYZE uruchamia zapytanie; EXPLAIN daje szacunek
ANALYZE SELECT ...;

-- SQLite: pokazuje, jak skanowane są tabele/indeksy
EXPLAIN QUERY PLAN SELECT ...;

Oracle rozbija to na dwa kroki — zapełnij tabelę planu, a potem ją sformatuj:

EXPLAIN PLAN FOR SELECT ...;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);

SQL Server przełącza wypisywanie planu ustawieniem sesji (albo przechwytujesz plan graficzny w SSMS przez Ctrl+M):

SET STATISTICS PROFILE ON;   -- liczby wierszy + plan dla każdej instrukcji
-- lub: SET SHOWPLAN_XML ON;  -- plan jako XML, bez wykonania

Kluczowe rozróżnienie: szacunki typu EXPLAIN nie uruchamiają zapytania, podczas gdy warianty „analyze” (EXPLAIN ANALYZE w PostgreSQL / MySQL 8.0.18+, ANALYZE w MariaDB) faktycznie je wykonują i raportują rzeczywiste liczby wierszy oraz czasy. Przy DELETE czy UPDATE ta różnica ma ogromne znaczenie. EXPLAIN ANALYZE w MySQL pojawiło się w 8.0.18 (MySQL blog: EXPLAIN ANALYZE).

Co jeszcze cię zaskoczy przy zmianie silnika?

Kilka mniejszych różnic odpowiada za zaskakująco dużą część momentów „dlaczego to nie działa”:

  • Konkatenacja tekstu. PostgreSQL, Oracle i SQLite używają ||. SQL Server używa +. MySQL i MariaDB używają funkcji CONCAT() — domyślnie || oznacza logiczne OR, a nie konkatenację, chyba że ustawisz PIPES_AS_CONCAT w sql_mode (MariaDB: PIPES_AS_CONCAT).
  • Bieżąca data/godzina. CURRENT_TIMESTAMP działa niemal wszędzie, ale nazwy specyficzne dla silników się różnią: NOW() (PostgreSQL, MySQL), GETDATE() / SYSDATETIME() (SQL Server), SYSDATE (Oracle), datetime('now') (SQLite).
  • Klucze auto-increment. GENERATED ... AS IDENTITY lub SERIAL (PostgreSQL), AUTO_INCREMENT (MySQL/MariaDB), IDENTITY (SQL Server), kolumny tożsamości lub sekwencje (Oracle), AUTOINCREMENT / niejawny rowid (SQLite).
  • Wartości logiczne. PostgreSQL ma prawdziwy typ BOOLEAN; BOOLEAN w MySQL to alias dla TINYINT(1); SQL Server używa BIT; SQLite przechowuje je jako 0 / 1. Klasyczny SQL w Oracle nie miał typu logicznego w ogóle.

Żadna z tych rzeczy nie jest trudna, gdy już ją znasz — problem w tym, że rzadko znasz je wszystkie dla wszystkich pięciu silników naraz.

Jak zachować świadomość dialektu bez zapamiętywania każdego niuansu?

Uczciwa odpowiedź brzmi: trzymaj pod ręką ściągę taką jak ta i korzystaj z narzędzi, które znają dialekt bazy, do której jesteś podłączony. Większość generycznych „AI do SQL” pisze jeden wariant SQL i zostawia tłumaczenie tobie — a to wprowadza z powrotem dokładnie te błędy opisane powyżej.

Ujawnienie: ten blog wydaje LumaSoft (LUMA sp. z o.o.), a ja pracuję nad narzędziem wspomnianym poniżej.

SQL Sage for DBeaver to plugin, który dodaje AI copilota wewnątrz DBeavera i jest świadomy dialektu tych samych pięciu silników — PostgreSQL, MySQL/MariaDB, Oracle, SQL Server i SQLite — więc składnia paginacji, cytowania i planów pasuje do połączenia, na którym pracujesz. Kilka faktów, które warto powiedzieć 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, nie tylko w interfejsie. Nie jest powiązany z DBeaver, Oracle ani Microsoft. Jak jeden copilot prowadzi pięć silników w jednym procesie pracy, pokazuje strona AI dla DBeaver (EN). Krótko o tym, z jakimi bazami działa, jak się logujesz i co trafia do modelu — na stronie SQL Sage dla DBeaver.

Niezależnie od tego, czy użyjesz narzędzia, czy własnej pamięci, wniosek jest ten sam: standard SQL przeprowadza cię przez większość drogi, a ostatnie 10% — paginacja, cytowanie, plany, konkatenacja — to miejsce, w którym naprawdę rozgrywa się praca między silnikami. Zachowaj tę stronę w zakładkach na następny raz, gdy „działające” zapytanie odmówi uruchomienia.

← Wróć do bloga