Blog JSystems - uwalniamy wiedzę!

Szukaj
Pisanie zapytań SQL z ChatGPT: zrzuty z ChatGPT i SQL Server Management Studio z części 1 przewodnika AI w SQL
Część 1 przewodnika AI w SQL. Na obrazie trzy prawdziwe zrzuty z tej części.

W skrócieZapytanie SQL od ChatGPT jest dobre wtedy, gdy model dostaje opis schematu bazy, dialekt z wersją serwera i reguły liczenia. Bez schematu wymyśla tabele i kolumny. Pokazujemy, jak przygotować opis schematu jednym zapytaniem, jak wymusić dialekt i jak sprawdzić wynik niezależnym zapytaniem.

To część 1 z 8 przewodnika AI w SQL. Wszystkie przykłady powstały na bazie DemoShop w SQL Server 2022: fikcyjnym sklepie internetowym z 2,4 mln zamówień. Polecenia wpisujemy do ChatGPT, ale te same możesz podać Microsoft Copilotowi, Claude albo Gemini. Przy skryptach korzystających z mechanizmów serwera w małej szarej ramce znajdziesz ich odpowiedniki dla Oracle i PostgreSQL.

Pliki do tej części

Skrypt bazy uruchamiasz w SSMS na serwerze SQL Server 2019 lub nowszym. Tworzy bazę DemoShop, na której powstały wszystkie przykłady.

Pisanie zapytań SQL z AI: od pytania do działającego kodu

Zacznijmy od tego, co robi większość osób przy pierwszym kontakcie z AI: wpisuje pytanie biznesowe tak, jak zadałaby je koledze. Chcemy dziesięciu klientów, którzy w 2025 roku wydali najwięcej.

Napisz zapytanie SQL, które pokaże 10 klientów, którzy w 2025 roku wydali u nas najwięcej. Chcę widzieć imię i nazwisko, liczbę zamówień i łączną kwotę zakupów.

ChatGPT pisze zapytanie SQL bez znajomości schematu bazy: zakłada tabele customers i orders z kolumną total_amount i kończy kod klauzulą LIMIT 10
ChatGPT odpowiada natychmiast i poprawnie składniowo, ale na wymyślonym schemacie: tabele customers i orders, kolumna total_amount, klauzula LIMIT. Uczciwie zaznacza „zakładając przykładowy schemat”. Łatwo to przeoczyć.

Odpowiedź wygląda profesjonalnie: zakres dat zapisany jako przedział zamiast funkcji na kolumnie, grupowanie po identyfikatorze klienta, sortowanie malejące. Problem w tym, że w DemoShop nie ma tabeli customers z kolumną id, nie ma kolumny total_amount, a wartość zamówienia trzeba policzyć z pozycji. Do tego LIMIT 10 to składnia MySQL i PostgreSQL. Wklejamy kod do SSMS i uruchamiamy klawiszem F5:

SELECT
    c.first_name,
    c.last_name,
    COUNT(o.id) AS liczba_zamowien,
    SUM(o.total_amount) AS laczna_kwota_zakupow
FROM customers c
JOIN orders o
    ON o.customer_id = c.id
WHERE o.order_date >= '2025-01-01'
  AND o.order_date < '2026-01-01'
GROUP BY
    c.id,
    c.first_name,
    c.last_name
ORDER BY
    laczna_kwota_zakupow DESC
LIMIT 10;
SQL Server Management Studio zwraca błąd 102 Incorrect syntax near LIMIT po uruchomieniu zapytania wygenerowanego przez AI bez schematu
SQL Server zatrzymuje się już na etapie analizy składni: komunikat 102, „Incorrect syntax near 'LIMIT'”. Gdybyśmy poprawili LIMIT na TOP, kolejnym błędem byłyby nieistniejące tabele i kolumny.

Ten błąd jest akurat niegroźny, bo widać go od razu. Groźniejszy scenariusz to baza, w której przypadkiem istnieje tabela orders z kolumną total_amount, ale liczoną inaczej, niż myślisz (na przykład z kosztem dostawy albo z zamówieniami anulowanymi). Wtedy zapytanie wykona się bez błędu i zwróci liczby, które wyglądają wiarygodnie. Dlatego zanim poprosisz AI o zapytanie, daj mu cztery rzeczy: dialekt i wersję serwera, opis schematu, reguły liczenia i instrukcję, co ma zrobić, gdy czegoś brakuje.

W poleceniu poniżej obok wersji serwera znajduje się poziom zgodności (compatibility level). To ustawienie samej bazy, które mówi, z którą wersją SQL Server baza ma się zachowywać zgodnie: 160 oznacza SQL Server 2022, 150 to SQL Server 2019, 140 to SQL Server 2017. Od poziomu zgodności zależy, które funkcje języka są dostępne i jak optymalizator buduje plany. Serwer bywa nowszy od bazy: baza przeniesiona na SQL Server 2022 często ma dalej poziom 150, żeby stare zapytania działały jak wcześniej. Dlatego AI powinno znać oba parametry. Poziom zgodności swojej bazy sprawdzisz zapytaniem SELECT name, compatibility_level FROM sys.databases;.

Piszesz zapytania w T-SQL dla Microsoft SQL Server 2022 (poziom zgodności 160). Korzystaj wyłącznie z tabel i kolumn opisanych w załączonym pliku demoshop_schemat_dla_ai.txt. Jeśli do odpowiedzi brakuje Ci informacji, zapytaj, zamiast zgadywać.

Zasady liczenia:
- kwota zakupów to suma wartości pozycji: Quantity * UnitPrice * (100 - DiscountPct) / 100, bez kosztu dostawy,
- zamówienia anulowane (Status = 'CANCELLED') nie liczą się do sprzedaży,
- rok liczymy według OrderDate (czas lokalny).

Zadanie: pokaż 10 klientów, którzy w 2025 roku wydali najwięcej. Kolumny: imię i nazwisko, liczba zamówień, łączna kwota zakupów. Posortuj od największej kwoty.

Plik demoshop_schemat_dla_ai.txt to opis wszystkich tabel bazy w 85 linijkach: kolumny z typami, klucze główne i obce, dopuszczalne wartości i opisy kolumn. Dla bazy DemoShop plik jest gotowy do pobrania. Dla własnej bazy wygenerujesz taki sam opis jednym zapytaniem: uruchamiasz w SSMS gotowy skrypt schemat_dla_ai.sql na swojej bazie, kopiujesz wynik i zapisujesz go do pliku .txt. Krok po kroku pokazujemy to w części Zwięzły opis schematu dla AI jednym zapytaniem, a opisy kolumn, które trafiają do pliku, dodajesz tak, jak w części Opisy kolumn w bazie. Odpowiedź ChatGPT ze schematem wygląda zupełnie inaczej:

ChatGPT z załączonym opisem schematu pisze poprawne zapytanie T-SQL z CTE OrderValues, TOP (10), zakresem dat i wykluczeniem zamówień anulowanych
Z opisem schematu ChatGPT najpierw nazywa tabele, z których skorzysta, potem pisze zapytanie z CTE (nazwanym podzapytaniem) OrderValues. Wartość liczy z pozycji, pomija zamówienia anulowane, używa TOP (10) i zakresu dat zgodnego z indeksem.

Całe zapytanie od ChatGPT do skopiowania i uruchomienia w SSMS na bazie DemoShop:

WITH OrderValues AS
(
    SELECT
        o.OrderID,
        o.CustomerID,
        SUM(
            CAST(oi.Quantity AS decimal(19, 4))
            * oi.UnitPrice
            * (100.0 - oi.DiscountPct) / 100.0
        ) AS OrderValue
    FROM dbo.Orders AS o
    INNER JOIN dbo.OrderItems AS oi
        ON oi.OrderID = o.OrderID
    WHERE
        o.OrderDate >= '20250101'
        AND o.OrderDate < '20260101'
        AND o.Status <> 'CANCELLED'
    GROUP BY
        o.OrderID,
        o.CustomerID
)
SELECT TOP (10)
    CONCAT(c.FirstName, N' ', c.LastName) AS [Imię i nazwisko],
    COUNT(*) AS [Liczba zamówień],
    SUM(ov.OrderValue) AS [Łączna kwota zakupów]
FROM OrderValues AS ov
INNER JOIN dbo.Customers AS c
    ON c.CustomerID = ov.CustomerID
GROUP BY
    c.CustomerID,
    c.FirstName,
    c.LastName
ORDER BY
    [Łączna kwota zakupów] DESC;
Wynik zapytania wygenerowanego przez AI w SSMS: 10 klientów DemoShop z największą kwotą zakupów w 2025 roku, z liczbą zamówień i łączną kwotą
Wynik w SSMS: dziesięciu klientów z największymi zakupami w 2025 roku. Na pierwszym miejscu Agnieszka Pawlak: 28 797 zamówień na 287,2 mln zł. Cała pierwsza dziesiątka to klienci firmowi (segment B2B), stąd tak duże liczby.

Zwróć uwagę na dwa szczegóły, które świadczą o tym, że AI przeczytało schemat, a nie zgadywało. Po pierwsze najpierw sumuje wartość każdego zamówienia w CTE, a dopiero potem liczy zamówienia klienta przez COUNT(*). Gdyby połączyło klientów od razu z pozycjami, każde zamówienie policzyłoby się tyle razy, ile ma pozycji (to błąd 1 z naszej listy dwudziestu typowych błędów: JOIN, który mnoży wiersze). Po drugie grupuje po CustomerID, a nie tylko po imieniu i nazwisku. To nie jest pedanteria: w DemoShop jest 40 klientów o imieniu i nazwisku Agnieszka Pawlak, w tym ta z pierwszego miejsca rankingu. Grupowanie po samym imieniu i nazwisku zsumowałoby ich zakupy w jedną pozycję. Jak bardzo zmienia to wynik, zobaczysz za chwilę w zapytaniu kontrolnym.

Sprawdzenie wyniku niezależnym zapytaniem

Sam fakt, że zapytanie się wykonało, niczego jeszcze nie dowodzi. Wynik sprawdzamy niezależną drogą: osobnym, prostszym zapytaniem, które liczy to samo inaczej. Obie liczby dla sprawdzanego klienta muszą się zgadzać co do grosza. Takie zapytanie kontrolne też możesz zamówić u ChatGPT. W nowej rozmowie załączamy ten sam plik ze schematem, wklejamy zapytanie z rankingu i liczby z pierwszego wiersza wyniku:

Piszesz zapytania w T-SQL dla Microsoft SQL Server 2022 (poziom zgodności 160). Korzystaj wyłącznie z tabel i kolumn opisanych w załączonym pliku demoshop_schemat_dla_ai.txt. Jeśli do odpowiedzi brakuje Ci informacji, zapytaj, zamiast zgadywać.

Zasady liczenia:
- kwota zakupów to suma wartości pozycji: Quantity * UnitPrice * (100 - DiscountPct) / 100, bez kosztu dostawy,
- zamówienia anulowane (Status = 'CANCELLED') nie liczą się do sprzedaży,
- rok liczymy według OrderDate (czas lokalny).

To zapytanie zwróciło 10 klientów, którzy w 2025 roku wydali najwięcej:

WITH OrderValues AS
(
    SELECT
        o.OrderID,
        o.CustomerID,
        SUM(
            CAST(oi.Quantity AS decimal(19, 4))
            * oi.UnitPrice
            * (100.0 - oi.DiscountPct) / 100.0
        ) AS OrderValue
    FROM dbo.Orders AS o
    INNER JOIN dbo.OrderItems AS oi
        ON oi.OrderID = o.OrderID
    WHERE
        o.OrderDate >= '20250101'
        AND o.OrderDate < '20260101'
        AND o.Status <> 'CANCELLED'
    GROUP BY
        o.OrderID,
        o.CustomerID
)
SELECT TOP (10)
    CONCAT(c.FirstName, N' ', c.LastName) AS [Imię i nazwisko],
    COUNT(*) AS [Liczba zamówień],
    SUM(ov.OrderValue) AS [Łączna kwota zakupów]
FROM OrderValues AS ov
INNER JOIN dbo.Customers AS c
    ON c.CustomerID = ov.CustomerID
GROUP BY
    c.CustomerID,
    c.FirstName,
    c.LastName
ORDER BY
    [Łączna kwota zakupów] DESC;

W pierwszym wierszu wyniku jest Agnieszka Pawlak: 28 797 zamówień i 287 215 170,80 zł. Napisz niezależne zapytanie kontrolne, które policzy te dwie liczby dla tego klienta inną drogą: bez CTE i bez rankingu, bezpośrednio z pozycji zamówień. Nie powielaj logiki zapytania powyżej.

ChatGPT pisze niezależne zapytanie kontrolne bez CTE z filtrem po imieniu i nazwisku klientki i ostrzega, że kombinacja FirstName i LastName nie jest unikalna
ChatGPT liczy obie wartości bez CTE, wprost z pozycji zamówień. Pod kodem sam zastrzega, że imię i nazwisko nie muszą być unikalne: jeśli w bazie jest więcej osób o tym imieniu i nazwisku, zapytanie zsumuje ich zakupy.

Zapytanie kontrolne od ChatGPT do skopiowania:

SELECT
    COUNT(DISTINCT o.OrderID) AS [Liczba zamówień],
    SUM(
        oi.Quantity * oi.UnitPrice
        * (1 - oi.DiscountPct / 100.0)
    ) AS [Łączna kwota zakupów]
FROM dbo.Customers AS c
JOIN dbo.Orders AS o
    ON o.CustomerID = c.CustomerID
JOIN dbo.OrderItems AS oi
    ON oi.OrderID = o.OrderID
WHERE
    c.FirstName = N'Agnieszka'
    AND c.LastName = N'Pawlak'
    AND o.OrderDate >= '20250101'
    AND o.OrderDate < '20260101'
    AND o.Status <> 'CANCELLED';
SSMS: zapytanie kontrolne z filtrem po imieniu i nazwisku Agnieszka Pawlak zwraca 28 923 zamówienia i 287 435 964,58 zł, więcej niż w rankingu
Kontrola po imieniu i nazwisku: 28 923 zamówienia i 287 435 964,58 zł. Ranking pokazywał 28 797 zamówień i 287 215 170,80 zł, więc liczby się nie zgadzają.

Liczby się nie zgadzają i właśnie po to robi się kontrolę. Zastrzeżenie ChatGPT było trafne: zapytanie z filtrem po imieniu i nazwisku zsumowało zakupy wszystkich 40 klientek o imieniu i nazwisku Agnieszka Pawlak, a ranking liczył jedną z nich, bo grupuje po CustomerID. Wracamy z prawdziwym wynikiem do tej samej rozmowy:

Uruchomiłem Twoje zapytanie kontrolne w SSMS: 28 923 zamówienia i 287 435 964,58 zł, czyli więcej niż w rankingu. Masz rację, w bazie jest 40 klientów o imieniu i nazwisku Agnieszka Pawlak. Popraw zapytanie kontrolne tak, żeby jednoznacznie sprawdzało klientkę z pierwszego wiersza rankingu.

ChatGPT zaleca dopisanie kolumny c.CustomerID do rankingu, żeby odczytać identyfikator klientki z pierwszego wiersza
Krok 1 według ChatGPT: dopisać c.CustomerID do listy kolumn rankingu i odczytać identyfikator z wiersza z 28 797 zamówieniami.
ChatGPT podaje zapytanie kontrolne z parametrem @CustomerID i oczekiwanym wynikiem 28 797 zamówień i 287 215 170,80 zł
Krok 2: zapytanie kontrolne po CustomerID. ChatGPT nie wpisuje identyfikatora sam, bo nie ma go ani w schemacie, ani w wyniku rankingu, i zaznacza, że oczekiwany wynik trzeba dopiero sprawdzić.

Robimy oba kroki. Do rankingu dopisujemy c.CustomerID zaraz pod SELECT TOP (10), resztę zapytania zostawiamy bez zmian:

SSMS: ranking 10 klientów z dopisaną kolumną CustomerID, Agnieszka Pawlak z pierwszego wiersza ma CustomerID 7
Ranking z kolumną CustomerID: Agnieszka Pawlak z pierwszego wiersza to klientka nr 7.

Identyfikator 7 wpisujemy do zapytania kontrolnego z kroku 2:

DECLARE @CustomerID int = 7; -- CustomerID z kroku 1

SELECT
    COUNT(DISTINCT o.OrderID) AS [Liczba zamówień],
    SUM(
        oi.Quantity * oi.UnitPrice
        * (1 - oi.DiscountPct / 100.0)
    ) AS [Łączna kwota zakupów]
FROM dbo.Orders AS o
INNER JOIN dbo.OrderItems AS oi
    ON oi.OrderID = o.OrderID
WHERE
    o.CustomerID = @CustomerID
    AND o.OrderDate >= '20250101'
    AND o.OrderDate < '20260101'
    AND o.Status <> 'CANCELLED';
SSMS: zapytanie kontrolne dla CustomerID 7 zwraca 28 797 zamówień i 287 215 170,7968 zł, tyle samo co ranking
Kontrola po CustomerID = 7: 28 797 zamówień i 287 215 170,7968 zł, czyli po zaokrągleniu 287 215 170,80 zł. Dokładnie tyle samo co w rankingu.

Teraz obie drogi dają ten sam wynik co do grosza, więc ranking jest potwierdzony. Ten przykład pokazuje dwie rzeczy naraz. Zapytanie kontrolne musi sprawdzać dokładnie ten sam obiekt, co zapytanie sprawdzane, czyli identyfikator, a nie nazwę. A rozbieżność w kontroli nie zawsze oznacza błąd w sprawdzanym zapytaniu: tutaj błąd był w kontroli i wyszedł dzięki temu, że wynik porównaliśmy z liczbą, a nie przyjęliśmy na wiarę. Tę technikę stosujemy w całym przewodniku i rozwijamy w sekcji o testach SQL.

Jak wymusić konkretny dialekt: PostgreSQL, SQL Server, Oracle, MySQL

Każda baza mówi trochę innym dialektem SQL. Najczęstsze różnice dotyczą ograniczania liczby wierszy, łączenia tekstu, literałów dat, funkcji na datach i typów danych. AI bez wskazówki wybiera dialekt, który najczęściej widziało w danych treningowych, i zwykle jest to składnia MySQL albo PostgreSQL. Wymuszenie dialektu jest proste: nazwij silnik i wersję w pierwszym zdaniu polecenia. Jeśli pracujesz z kilkoma bazami, poproś o wszystkie wersje naraz i o tabelę różnic, która od razu uczy, na co uważać.

Na podstawie schematu z załączonego pliku demoshop_schemat_dla_ai.txt napisz to samo zestawienie w czterech dialektach: Microsoft SQL Server 2022, PostgreSQL 16, Oracle Database 23ai i MySQL 8.0.

Zestawienie: 10 klientów z najwyższą kwotą zakupów w 2025 roku (imię i nazwisko, liczba zamówień, kwota).

Zasady liczenia:
- kwota zakupów to suma wartości pozycji: Quantity * UnitPrice * (100 - DiscountPct) / 100, bez kosztu dostawy,
- zamówienia anulowane (Status = 'CANCELLED') nie liczą się do sprzedaży,
- rok liczymy według OrderDate (czas lokalny).

Każdą wersję podaj w osobnym bloku kodu z nazwą dialektu w nagłówku. Pod blokami wypisz w tabeli, czym różni się składnia: ograniczenie liczby wierszy, łączenie tekstu i literały dat.

ChatGPT pisze to samo zapytanie w czterech dialektach SQL: SQL Server 2022, PostgreSQL 16, Oracle 23ai i MySQL 8.0, z tabelą różnic składni
Tabela różnic pod czterema wersjami kodu: TOP (10) w SQL Server, LIMIT w PostgreSQL i MySQL, FETCH FIRST 10 ROWS ONLY w Oracle; operator + w SQL Server, || w PostgreSQL i Oracle, CONCAT w MySQL.

Wersje różnią się dokładnie tam, gdzie powinny. Teoria to jednak za mało, więc każdą z czterech wersji uruchomiliśmy w jej własnym silniku i jego własnym kliencie wiersza poleceń, na tych samych danych z 2025 roku: SQL Server 2022 (sqlcmd), PostgreSQL 15 (psql), Oracle AI Database 26ai Free (SQL*Plus) i MySQL 8.0. AI pisało dla PostgreSQL 16 i Oracle 23ai, ale użyta składnia jest w tych wersjach taka sama. Wszystkie cztery zwróciły tych samych dziesięciu klientów z identycznymi liczbami zamówień i kwotami. Jedyna różnica widoczna na ekranie to nagłówki w PostgreSQL: customername zamiast CustomerName. PostgreSQL zamienia na małe litery każdą nazwę zapisaną bez cudzysłowów, także nazwy tabel. Zapytanie od AI zadziałało, bo tabele w PostgreSQL też utworzyliśmy bez cudzysłowów; przy tabelach założonych jako "Customers" skończyłoby się błędem „relation does not exist”. Tę i inne pułapki przenoszenia kodu między bazami opisujemy w sekcji o konwersji dialektów.

Ten sam ranking dziesięciu klientów uruchomiony w czterech bazach przez sqlcmd, psql, SQL*Plus i klienta mysql, z identycznymi wynikami
Cztery wersje od AI uruchomione w czterech silnikach na tych samych danych. Liczby zamówień i kwoty zgadzają się co do grosza, różni się tylko liczba miejsc po przecinku i wielkość liter w nagłówkach.

Jak zmusić AI, żeby nie wymyślało tabel i kolumn

Model językowy zawsze coś odpowie. Jeśli zapytasz o dane, których w bazie nie ma, nie powie sam z siebie „takiej tabeli nie ma”, tylko dopisze ją do odpowiedzi. Sprawdziliśmy to na wskaźniku NPS (Net Promoter Score, wynik ankiety „czy polecisz nas znajomym” w skali 0-10). W DemoShop nie ma żadnych danych z ankiet. Najpierw zwykłe polecenie, bez schematu i bez zasad:

Napisz zapytanie SQL: 20 klientów z najwyższym wskaźnikiem NPS w 2025 roku razem z kwotą ich zakupów.

ChatGPT bez schematu wymyśla tabelę nps_responses z kolumną nps_score i pisze do niej zapytanie SQL
Bez schematu i bez zasad ChatGPT dopisuje do bazy tabelę nps_responses z kolumnami customer_id, response_date i nps_score. Kod jest przemyślany, łącznie z klasyczną definicją NPS, tylko że opisuje bazę, która nie istnieje.

Teraz to samo zadanie z czterema elementami, które w praktyce wystarczają, żeby AI przestało wymyślać: listą dozwolonych tabel (plik ze schematem), zakazem używania czegokolwiek spoza niej, prośbą o wypisanie użytych kolumn przed napisaniem kodu i zgodą na odpowiedź „nie da się”.

Piszesz zapytania w T-SQL dla Microsoft SQL Server 2022 (poziom zgodności 160). Korzystaj wyłącznie z tabel i kolumn opisanych w załączonym pliku demoshop_schemat_dla_ai.txt. Jeśli do odpowiedzi brakuje Ci informacji, zapytaj, zamiast zgadywać. Zanim napiszesz zapytanie, wypisz listę tabel i kolumn, których użyjesz. Jeśli czegoś nie ma w schemacie, napisz to wprost.

Zadanie: 20 klientów z najwyższym wskaźnikiem NPS w 2025 roku razem z kwotą ich zakupów.

ChatGPT ze schematem i zasadami odpowiada, że w bazie nie ma danych NPS, wypisuje kolumny do części zakupowej i prosi o wskazanie tabeli z wynikami ankiet
Ten sam model z jasnymi zasadami: „Do tego zadania nie da się napisać poprawnego zapytania wyłącznie na podstawie podanego schematu”. Wypisuje, czego użyłby do kwoty zakupów, i prosi o tabelę z ocenami NPS.

Zdanie „jeśli czegoś nie ma w schemacie, napisz to wprost” kosztuje kilka sekund, a zamienia zgadywanie w pytanie. Ta sama reguła przydała się nam w sekcji o JOIN-ach, gdzie ChatGPT dwa razy zamiast pisać kod dopytał, jak rozumieć polecenie.

Wersja serwera ma znaczenie: GENERATE_SERIES i poziom zgodności

Ostatnia pułapka przy pisaniu zapytań z AI to wersja serwera. Model zna najnowsze funkcje i chętnie ich używa, a w firmach wciąż pracują serwery sprzed kilku lat. Prosimy o dzienną liczbę zamówień w grudniu 2025 roku, z zerami w dniach bez zamówień. W naszym poleceniu znajduje się „SQL Server 2022, poziom zgodności 160”, więc ChatGPT sięga po funkcję GENERATE_SERIES, która generuje ciąg liczb, tu numery dni:

ChatGPT generuje kalendarz dni grudnia funkcją GENERATE_SERIES z SQL Server 2022 i łączy go z tabelą zamówień przez LEFT JOIN
Odpowiedź dla SQL Server 2022: GENERATE_SERIES(0, 30) tworzy 31 dni, LEFT JOIN do zamówień i COUNT(o.OrderID) zwraca zera dla dni bez zamówień.

Teraz symulujemy produkcję: bazę na poziomie zgodności 150, czyli takim, jak w SQL Server 2019. Jak pisaliśmy wyżej, o dostępnych funkcjach decyduje poziom zgodności bazy, a nie sama wersja serwera. Uruchamiamy ten sam kod:

-- serwer produkcyjny to SQL Server 2019: ustawiamy ten sam poziom zgodności
ALTER DATABASE DemoShop SET COMPATIBILITY_LEVEL = 150;
GO
SELECT
    d.OrderDay,
    COUNT(o.OrderID) AS OrderCount
FROM (
    SELECT DATEADD(DAY, value, CAST('2025-12-01' AS date)) AS OrderDay
    FROM GENERATE_SERIES(0, 30)
) AS d
LEFT JOIN dbo.Orders AS o
    ON o.OrderDate >= d.OrderDay
   AND o.OrderDate < DATEADD(DAY, 1, d.OrderDay)
GROUP BY
    d.OrderDay
ORDER BY
    d.OrderDay;
SSMS zwraca błąd Invalid object name GENERATE_SERIES po przełączeniu bazy na poziom zgodności 150
Po przełączeniu bazy na poziom zgodności 150 SQL Server nie rozpoznaje GENERATE_SERIES: komunikat 208, „Invalid object name 'GENERATE_SERIES'”. Na prawdziwym SQL Server 2019 komunikat byłby taki sam.

Nasz serwer produkcyjny to SQL Server 2019, a baza ma poziom zgodności 150. Czy to zapytanie tam zadziała? Jeśli nie, przepisz je tak, żeby działało.

ChatGPT wyjaśnia, że GENERATE_SERIES wymaga SQL Server 2022 i poziomu zgodności 160, i przepisuje kalendarz na rekurencyjne CTE
ChatGPT od razu wskazuje przyczynę i proponuje rekurencyjne CTE, które generuje kolejne dni aż do 31 grudnia. Dodaje, że limit rekurencji (domyślnie 100) przy 31 dniach nie jest problemem.
WITH Days AS
(
    SELECT CAST('2025-12-01' AS date) AS OrderDay

    UNION ALL

    SELECT DATEADD(DAY, 1, OrderDay)
    FROM Days
    WHERE OrderDay < '2025-12-31'
)
SELECT
    d.OrderDay,
    COUNT(o.OrderID) AS OrderCount
FROM Days AS d
LEFT JOIN dbo.Orders AS o
    ON o.OrderDate >= d.OrderDay
   AND o.OrderDate < DATEADD(DAY, 1, d.OrderDay)
GROUP BY
    d.OrderDay
ORDER BY
    d.OrderDay;
Wynik rekurencyjnego CTE w SSMS na poziomie zgodności 150: 31 dni grudnia 2025 z liczbą zamówień w każdym dniu
Wersja z rekurencyjnym CTE działa na poziomie zgodności 150 i zwraca 31 wierszy, po jednym na każdy dzień grudnia.

Wniosek praktyczny: w każdym poleceniu, które ma trafić na produkcję, podawaj wersję serwera i poziom zgodności bazy. Oba sprawdzisz jednym zapytaniem: SELECT @@VERSION, compatibility_level FROM sys.databases WHERE name = DB_NAME();.

Oracle i PostgreSQL: ten sam skrypt dla innych baz znajdziesz w plikach oracle_wersja_i_zgodnosc.sql i postgresql_wersja_i_zgodnosc.sql (wersja serwera, ustawienia zgodności i kalendarz dni bez GENERATE_SERIES).

Jak przekazać AI schemat bazy w SQL Server (krok po kroku)

Schemat to dla AI to samo, co mapa dla kierowcy. Bez niego model zgaduje nazwy kolumn i ścieżki złączeń, z nim pisze zapytania pod Twoją bazę. Zanim jednak cokolwiek wyślesz, warto samemu zobaczyć, jak tabele wyglądają i jak są ze sobą powiązane. SQL Server daje na to kilka dróg. Pokazujemy je od najprostszej do najbardziej przydatnej przy pracy z AI.

Krok 1. Eksplorator obiektów: kolumny, klucze, ograniczenia i indeksy

W SSMS po lewej stronie jest Eksplorator obiektów (Object Explorer, skrót F8). Rozwijasz kolejno: serwer, Bazy danych, DemoShop, Tabele, wybraną tabelę i folder Kolumny. Przy każdej kolumnie SSMS pokazuje typ danych, informację, czy dopuszcza NULL, i oznaczenia kluczy: PK to klucz główny (unikalny identyfikator wiersza), FK to klucz obcy, czyli kolumna wskazująca na wiersz w innej tabeli. Klucze obce to dla AI najważniejsza informacja, bo mówią, po czym łączyć tabele.

Eksplorator obiektów SSMS z rozwiniętą tabelą dbo.Orders: kolumny z typami danych, oznaczeniami PK i FK oraz informacją o wartościach null
Kolumny tabeli dbo.Orders w Eksploratorze obiektów. OrderID jest kluczem głównym (PK), CustomerID i EmployeeID są kluczami obcymi (FK), więc to po nich łączymy zamówienia z klientami i pracownikami.

Foldery Klucze, Ograniczenia i Indeksy pod tą samą tabelą pokazują nazwy kluczy obcych (z nazwy zwykle widać, dokąd prowadzą), ograniczenia CHECK, czyli reguły dopuszczalnych wartości, oraz indeksy, które mają znaczenie przy optymalizacji.

Foldery Klucze, Ograniczenia i Indeksy tabeli dbo.Orders w Eksploratorze obiektów SSMS: klucze obce do klientów i pracowników, ograniczenie CK_Orders_Status i indeksy
Klucze tabeli Orders: główny PK_Orders, obce FK_Orders_Customers i FK_Orders_Employees, unikalny UQ_Orders_OrderNumber. Ograniczenie CK_Orders_Status pilnuje listy statusów, a w Indeksach widać, że na kolumnie CustomerID nie ma indeksu. Wrócimy do tego przy optymalizacji.

Krok 2. sp_help: cała tabela w jednym wyniku

Klikanie po drzewie jest wygodne dla jednej tabeli, ale wolne dla dziesięciu. Procedura systemowa sp_help zwraca wszystko naraz: kolumny z typami i długościami, kolumnę IDENTITY (automatycznie numerowaną), indeksy, ograniczenia, a na końcu listę tabel, które wskazują na tę tabelę swoimi kluczami obcymi.

EXEC sp_help 'dbo.Orders';
Wynik procedury sp_help dla tabeli dbo.Orders w SSMS: kolumny i typy, kolumna IDENTITY, indeksy i ograniczenia w kilku siatkach wyników
sp_help zwraca kilka siatek wyników jedna pod drugą: kolumny, IDENTITY, indeksy, ograniczenia i odwołania z innych tabel. Wynik zaznaczasz Ctrl+A i kopiujesz razem z nagłówkami kolumn skrótem Ctrl+Shift+C.
Oracle i PostgreSQL: ten sam skrypt dla innych baz znajdziesz w plikach oracle_struktura_tabeli.sql i postgresql_struktura_tabeli.sql (kolumny, ograniczenia, indeksy i rozmiar jednej tabeli, jak sp_help).

Krok 3. Wszystkie relacje w bazie jednym zapytaniem

Do pisania złączeń AI potrzebuje pełnej mapy: która kolumna w której tabeli wskazuje na którą. Tę mapę zwracają widoki systemowe sys.foreign_keys i sys.foreign_key_columns. Poniższe zapytanie wypisuje wszystkie relacje w bazie, po jednej w wierszu:

/* Wszystkie relacje (klucze obce) w bazie: kto na kogo wskazuje */
SELECT fk.name                                   AS ForeignKey,
       OBJECT_SCHEMA_NAME(fk.parent_object_id) + '.' + OBJECT_NAME(fk.parent_object_id)
           + '.' + pc.name                       AS ChildColumn,
       OBJECT_SCHEMA_NAME(fk.referenced_object_id) + '.' + OBJECT_NAME(fk.referenced_object_id)
           + '.' + rc.name                       AS ParentColumn,
       fk.delete_referential_action_desc         AS OnDelete
FROM sys.foreign_keys AS fk
JOIN sys.foreign_key_columns AS fkc ON fkc.constraint_object_id = fk.object_id
JOIN sys.columns AS pc ON pc.object_id = fkc.parent_object_id
                      AND pc.column_id = fkc.parent_column_id
JOIN sys.columns AS rc ON rc.object_id = fkc.referenced_object_id
                      AND rc.column_id = fkc.referenced_column_id
ORDER BY ParentColumn, ChildColumn;
Wynik zapytania do sys.foreign_keys w SSMS: 12 relacji bazy DemoShop z kolumną podrzędną i nadrzędną każdego klucza obcego
Dwanaście relacji DemoShop. Każdy wiersz to jedna ścieżka złączenia, na przykład dbo.Orders.CustomerID wskazuje na dbo.Customers.CustomerID, a dbo.Categories.ParentCategoryID na dbo.Categories.CategoryID, czyli kategoria nadrzędna.
Oracle i PostgreSQL: ten sam skrypt dla innych baz znajdziesz w plikach oracle_relacje_klucze_obce.sql i postgresql_relacje_klucze_obce.sql.

Krok 4. Diagram bazy danych

Jeśli wolisz obraz, SSMS narysuje diagram relacji. W Eksploratorze obiektów kliknij prawym przyciskiem folder Diagramy bazy danych pod bazą i wybierz Nowy diagram bazy danych. Przy pierwszym użyciu SSMS zapyta o zainstalowanie obsługi diagramów, czyli kilku obiektów systemowych w bazie; na bazie produkcyjnej uzgodnij to z administratorem.

Menu kontekstowe folderu Diagramy bazy danych w Eksploratorze obiektów SSMS z poleceniem Nowy diagram bazy danych
Polecenie Nowy diagram bazy danych w menu folderu Diagramy bazy danych.
Okno Dodaj tabelę w edytorze diagramów SSMS z listą tabel bazy DemoShop
W oknie Dodaj tabelę zaznaczasz tabele (z klawiszem Shift wszystkie naraz) i klikasz Dodaj.
Diagram relacji bazy DemoShop w SSMS: tabele Orders, Customers, Regions, Employees, Payments, OrderItems, Products, Categories, Returns i Shipments połączone liniami kluczy obcych
Diagram DemoShop. Linie łączą klucze obce z kluczami głównymi; kluczyk oznacza stronę „jeden”, symbol nieskończoności stronę „wiele”. Widać od razu, że do zwrotów (Returns) prowadzi droga przez OrderItems, a nie bezpośrednio przez Orders.

Diagram świetnie się nadaje do zrozumienia bazy przez człowieka. Dla AI zrzut diagramu to gorszy wybór niż tekst: model musi odczytać nazwy z obrazka, a przy kilkudziesięciu tabelach linie się krzyżują. Tekst jest jednoznaczny i zajmuje mniej miejsca w rozmowie.

Krok 5. Skrypt CREATE jednej tabeli

Najdokładniejszy opis tabeli to jej definicja w SQL. Kliknij tabelę prawym przyciskiem i wybierz Tabela skryptu jako, UTWÓRZ, Nowe okno edytora zapytań.

Menu Tabela skryptu jako, UTWÓRZ, Nowe okno edytora zapytań w Eksploratorze obiektów SSMS
Generowanie skryptu CREATE TABLE dla jednej tabeli z menu kontekstowego.
Skrypt CREATE TABLE dla tabeli dbo.Orders wygenerowany przez SSMS, z kolumnami, kluczem głównym i opcjami indeksów
Skrypt CREATE TABLE tabeli Orders. Zawiera wszystko, ale też sporo szumu: opcje SET, opcje indeksów (PAD_INDEX, STATISTICS_NORECOMPUTE i inne) i nazwy grup plików. Dla AI ten szum jest zbędny.

Krok 6. Generowanie skryptów dla wielu tabel

Dla całej bazy użyj kreatora: prawy przycisk na bazie, Zadania, Wygeneruj skrypty. W kroku Wybierz obiekty zaznacz tabele, które mają trafić do AI. Nie wysyłaj wszystkiego z przyzwyczajenia: tabele techniczne, archiwalne i nieużywane w danym zadaniu tylko rozpraszają model.

Menu Zadania, Wygeneruj skrypty dla bazy DemoShop w Eksploratorze obiektów SSMS
Kreator generowania skryptów uruchamiasz z menu Zadania bazy danych.
Kreator Generuj skrypty w SSMS, krok Wybierz obiekty, z zaznaczonymi tabelami bazy DemoShop
Krok Wybierz obiekty: zaznaczamy tylko tabele, bez widoków, procedur i użytkowników.

W kroku Opcje wykonywania skryptów kliknij Zaawansowane i sprawdź opcję Typy danych do skryptu. Musi być ustawiona na Tylko schemat. Wartość „Schemat i dane” dopisałaby do pliku polecenia INSERT ze wszystkimi wierszami, czyli do AI trafiłaby zawartość bazy, a nie jej struktura.

Zaawansowane opcje kreatora Generuj skrypty w SSMS z opcją Typy danych do skryptu ustawioną na Tylko schemat
Opcja Typy danych do skryptu: Tylko schemat. To zabezpieczenie przed przypadkowym wysłaniem danych zamiast definicji tabel.
Krok zapisu skryptów w kreatorze SSMS: zapis do jednego pliku SQL
Zapis wszystkich definicji do jednego pliku .sql, który możesz załączyć do rozmowy z AI.
Oracle i PostgreSQL: ten sam skrypt dla innych baz znajdziesz w plikach oracle_ddl_tabel.sql i postgresql_ddl_tabel.sql (skrypt CREATE jednej tabeli i całego schematu).

Krok 7. Zwięzły opis schematu dla AI jednym zapytaniem

Skrypty CREATE są kompletne, ale rozwlekłe. Dlatego do rozmów z AI przygotowaliśmy zapytanie, które składa opis w formie, jaką model czyta najłatwiej: nagłówek tabeli z liczbą wierszy i opisem, a pod nim kolumny z typem, informacją o NULL, kluczach i dokąd prowadzi klucz obcy, oraz dopuszczalne wartości z ograniczeń CHECK. Zapytanie jest do pobrania jako schemat_dla_ai.sql. Czyta wyłącznie widoki systemowe, więc niczego w bazie nie zmienia i możesz je uruchomić także na produkcji.

/* Opis schematu bazy dla AI: tabele, kolumny, typy, klucze, relacje,
   dopuszczalne wartości (CHECK) i opisy kolumn (MS_Description). */
WITH t AS (
    SELECT t.object_id,
           TableName = SCHEMA_NAME(t.schema_id) + N'.' + t.name,
           RowCnt    = (SELECT SUM(p.rows) FROM sys.partitions AS p
                        WHERE p.object_id = t.object_id AND p.index_id IN (0, 1)),
           Descr     = (SELECT CAST(ep.value AS nvarchar(400)) FROM sys.extended_properties AS ep
                        WHERE ep.major_id = t.object_id AND ep.minor_id = 0 AND ep.name = N'MS_Description')
    FROM sys.tables AS t
    WHERE t.is_ms_shipped = 0
      AND NOT EXISTS (SELECT 1 FROM sys.extended_properties AS x   -- pomijamy np. dbo.sysdiagrams
                      WHERE x.major_id = t.object_id AND x.name = N'microsoft_database_tools_support')
)
SELECT SchemaForAI
FROM (
    /* nagłówek tabeli: nazwa, liczba wierszy, opis */
    SELECT t.TableName, Ord = 0,
           SchemaForAI = t.TableName + N' (' + REPLACE(FORMAT(t.RowCnt, N'N0', N'en-US'), N',', N' ')
                         + N' wierszy)' + ISNULL(N': ' + t.Descr, N'')
    FROM t
    UNION ALL
    /* kolumny: typ, NULL, klucz główny, klucz obcy, opis */
    SELECT t.TableName, Ord = c.column_id,
           SchemaForAI = N'    ' + c.name + N' ' + TYPE_NAME(c.user_type_id)
             + CASE WHEN TYPE_NAME(c.user_type_id) IN (N'varchar', N'char')
                         THEN N'(' + IIF(c.max_length = -1, N'max', CAST(c.max_length AS nvarchar(10))) + N')'
                    WHEN TYPE_NAME(c.user_type_id) IN (N'nvarchar', N'nchar')
                         THEN N'(' + IIF(c.max_length = -1, N'max', CAST(c.max_length / 2 AS nvarchar(10))) + N')'
                    WHEN TYPE_NAME(c.user_type_id) IN (N'decimal', N'numeric')
                         THEN N'(' + CAST(c.precision AS nvarchar(3)) + N',' + CAST(c.scale AS nvarchar(3)) + N')'
                    WHEN TYPE_NAME(c.user_type_id) IN (N'datetime2', N'datetimeoffset', N'time')
                         THEN N'(' + CAST(c.scale AS nvarchar(3)) + N')'
                    ELSE N'' END
             + IIF(c.is_nullable = 1, N' NULL', N' NOT NULL')
             + IIF(c.is_identity = 1, N' IDENTITY', N'')
             + IIF(EXISTS (SELECT 1 FROM sys.indexes AS i
                           JOIN sys.index_columns AS ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id
                           WHERE i.object_id = c.object_id AND i.is_primary_key = 1 AND ic.column_id = c.column_id),
                   N' PK', N'')
             + ISNULL((SELECT TOP (1) N' FK -> ' + OBJECT_SCHEMA_NAME(fkc.referenced_object_id) + N'.'
                              + OBJECT_NAME(fkc.referenced_object_id) + N'.' + rc.name
                       FROM sys.foreign_key_columns AS fkc
                       JOIN sys.columns AS rc ON rc.object_id = fkc.referenced_object_id
                                             AND rc.column_id = fkc.referenced_column_id
                       WHERE fkc.parent_object_id = c.object_id AND fkc.parent_column_id = c.column_id), N'')
             + ISNULL((SELECT N'  -- ' + CAST(ep.value AS nvarchar(400)) FROM sys.extended_properties AS ep
                       WHERE ep.major_id = c.object_id AND ep.minor_id = c.column_id
                         AND ep.name = N'MS_Description'), N'')
    FROM t
    JOIN sys.columns AS c ON c.object_id = t.object_id
    UNION ALL
    /* dopuszczalne wartości z ograniczeń CHECK */
    SELECT t.TableName, Ord = 1000 + ROW_NUMBER() OVER (PARTITION BY t.TableName ORDER BY cc.name),
           SchemaForAI = N'    CHECK ' + cc.definition
    FROM t
    JOIN sys.check_constraints AS cc ON cc.parent_object_id = t.object_id
) AS x
ORDER BY x.TableName, x.Ord;
Wynik zapytania składającego opis schematu dla AI w SSMS: tabele DemoShop z liczbą wierszy, kolumny z typami, kluczami głównymi i obcymi oraz opisami
Wynik zapytania w trybie tekstowym. Tabela dbo.Orders: 2 399 996 wierszy i opis „Jeden wiersz = jedno zamówienie”, przy kolumnach strzałki FK do tabel nadrzędnych i opisy znaczenia, na przykład że OrderDate to czas lokalny.

Cały opis DemoShop zajmuje 85 linijek i niecałe 5 KB, a mimo to zawiera wszystko, czego AI potrzebuje do pisania złączeń: jedenaście tabel, ich rozmiar, kolumny, klucze i relacje. Wynik najwygodniej skopiować w trybie tekstowym (Ctrl+T przed uruchomieniem zapytania) albo zaznaczyć w siatce i skopiować Ctrl+C, a potem zapisać do pliku .txt i załączyć do rozmowy. Plik, którego używaliśmy we wszystkich przykładach, jest do pobrania: demoshop_schemat_dla_ai.txt.

Dlaczego liczba wierszy? Dla poprawności zapytania jest zbędna, ale przy optymalizacji zmienia wszystko. Złączenie tabeli z 16 wierszami i tabeli z 6,5 mln wierszy wymaga innego podejścia niż złączenie dwóch małych słowników. AI, które zna rozmiary tabel, przestaje proponować rozwiązania dobre dla tysiąca wierszy i katastrofalne dla miliona.
Oracle i PostgreSQL: ten sam skrypt dla innych baz znajdziesz w plikach oracle_schemat_dla_ai.sql i postgresql_schemat_dla_ai.sql (wynik w tym samym formacie co dla SQL Server).

Krok 8. Opisy kolumn w bazie: dokumentacja, którą AI przeczyta

Typ varchar(12) nie powie AI, że to kod klienta, który dla firm ma prefiks „B2B-”. Takie znaczenie najlepiej zapisać w samej bazie jako właściwość rozszerzoną MS_Description. SSMS pokazuje ją we właściwościach kolumny, a nasze zapytanie dokleja ją do opisu po znakach --. Dopisanie opisu to jedno wywołanie procedury:

/* Opis kolumny zapisany w samej bazie - trafi do AI razem ze schematem */
EXEC sys.sp_addextendedproperty
     @name = N'MS_Description',
     @value = N'Kod promocji użyty w zamówieniu. NULL = zamówienie bez promocji.',
     @level0type = N'SCHEMA', @level0name = N'dbo',
     @level1type = N'TABLE',  @level1name = N'Orders',
     @level2type = N'COLUMN', @level2name = N'PromoCode';

SELECT c.name AS ColumnName,
       CAST(ep.value AS nvarchar(200)) AS Description
FROM sys.extended_properties AS ep
JOIN sys.columns AS c ON c.object_id = ep.major_id AND c.column_id = ep.minor_id
WHERE ep.major_id = OBJECT_ID(N'dbo.Orders') AND ep.name = N'MS_Description'
ORDER BY c.column_id;
Lista opisów kolumn tabeli dbo.Orders zapisanych jako MS_Description, wyświetlona zapytaniem do sys.extended_properties w SSMS
Opisy kolumn tabeli Orders po dodaniu opisu PromoCode. Każdy taki opis to jedno zdanie, które oszczędza AI zgadywania, a nowym osobom w zespole szukania po dokumentacji.

Opisy warto dodawać przede wszystkim tam, gdzie nazwa kolumny wprowadza w błąd albo kryje regułę: statusy i ich znaczenie, jednostki i waluty, strefy czasowe, pola, w których NULL coś oznacza, kolumny „IsActive”, które nie znaczą tego, co sugeruje nazwa. W DemoShop opis Customers.IsActive mówi wprost: „1 = konto nieusunięte i niezablokowane. To NIE oznacza, że klient ostatnio coś kupił”. Bez tego zdania AI zapytane o aktywnych klientów policzyłoby konta, a nie kupujących.

Oracle i PostgreSQL: ten sam skrypt dla innych baz znajdziesz w plikach oracle_opisy_kolumn.sql i postgresql_opisy_kolumn.sql (komentarze COMMENT ON zamiast MS_Description).

Krok 9. AI ze schematem pisze złączenia przez kilka tabel

Sprawdźmy opis w akcji na zadaniu, które wymaga czterech tabel i jednej reguły biznesowej: przychód w podziale na województwo klienta i kanał sprzedaży, z osobną grupą dla klientów bez województwa.

Piszesz zapytania w T-SQL dla Microsoft SQL Server 2022 (poziom zgodności 160). Korzystaj wyłącznie z tabel i kolumn opisanych w załączonym pliku demoshop_schemat_dla_ai.txt. Jeśli do odpowiedzi brakuje Ci informacji, zapytaj, zamiast zgadywać.

Zasady liczenia:
- kwota zakupów to suma wartości pozycji: Quantity * UnitPrice * (100 - DiscountPct) / 100, bez kosztu dostawy,
- zamówienia anulowane (Status = 'CANCELLED') nie liczą się do sprzedaży,
- rok liczymy według OrderDate (czas lokalny).

Zadanie: przychód w 2025 roku w podziale na województwo klienta i kanał sprzedaży. Klienci spoza Polski i bez województwa w osobnej grupie „brak województwa”. Posortuj malejąco według przychodu.

ChatGPT pisze zapytanie T-SQL łączące Orders, OrderItems, Customers i Regions z grupą brak województwa dla klientów spoza Polski
ChatGPT najpierw wyjaśnia ścieżkę: województwo z Customers.RegionID przez Regions, kraj z Customers.Country, kanał z Orders. Do Regions łączy przez LEFT JOIN, żeby nie zgubić klientów bez województwa, i składa grupę „brak województwa” w CASE.

Najważniejszy szczegół to LEFT JOIN dbo.Regions. Złączenie wewnętrzne (INNER JOIN) wyrzuciłoby z wyniku wszystkich klientów bez województwa, a więc całą grupę, o którą prosiliśmy, i część przychodu. AI wybrało poprawnie, bo opis schematu mówi, że RegionID dopuszcza NULL. Wynik w SSMS:

SELECT
    CASE
        WHEN c.Country = 'PL' AND c.RegionID IS NOT NULL
            THEN r.RegionName
        ELSE N'brak województwa'
    END AS Wojewodztwo,
    o.Channel AS KanalSprzedazy,
    SUM(
        oi.Quantity * oi.UnitPrice
        * (CAST(100 AS decimal(5,1)) - oi.DiscountPct)
        / CAST(100 AS decimal(5,1))
    ) AS Przychod
FROM dbo.Orders AS o
JOIN dbo.OrderItems AS oi
    ON oi.OrderID = o.OrderID
JOIN dbo.Customers AS c
    ON c.CustomerID = o.CustomerID
LEFT JOIN dbo.Regions AS r
    ON r.RegionID = c.RegionID
WHERE o.OrderDate >= '20250101'
  AND o.OrderDate <  '20260101'
  AND o.Status <> 'CANCELLED'
GROUP BY
    CASE
        WHEN c.Country = 'PL' AND c.RegionID IS NOT NULL
            THEN r.RegionName
        ELSE N'brak województwa'
    END,
    o.Channel
ORDER BY
    Przychod DESC;
Wynik w SSMS: przychód 2025 w podziale na województwo i kanał sprzedaży, z grupą brak województwa
51 wierszy: 16 województw razy 3 kanały plus grupa „brak województwa” w trzech kanałach. Suma wszystkich wierszy, policzona osobnym zapytaniem, to 2 283 093 002,77 zł, dokładnie tyle, ile wynosi cały przychód 2025 roku.

Jakie informacje o danych i schemacie przekazać AI

Poniższa lista to wszystko, co w tym przewodniku okazało się potrzebne, żeby AI pisało zapytania poprawne za pierwszym razem. Nie każde zadanie wymaga wszystkiego, ale każdy z tych punktów był przyczyną przynajmniej jednej złej odpowiedzi, gdy go zabrakło.

InformacjaPo co AISkąd ją wziąć w SQL Server
Silnik, wersja i poziom zgodnościDobór składni i funkcji (TOP czy LIMIT, GENERATE_SERIES czy CTE)SELECT @@VERSION, sys.databases.compatibility_level
Tabele, kolumny i typyBrak wymyślonych nazw, poprawne konwersje typówZapytanie schemat_dla_ai.sql, Wygeneruj skrypty
Klucze główne i obceŚcieżki złączeń, unikanie złączeń po złych kolumnachsys.foreign_keys, diagram
Liczności relacji (1:1, 1:N)Ochrona przed mnożeniem wierszy w złączeniachOpis tabeli („jedno zamówienie = najwyżej jedna wysyłka”)
Dopuszczalne wartościPoprawne filtry statusów i kodówOgraniczenia CHECK, tabele słownikowe
Znaczenie kolumn i reguły biznesoweWłaściwe definicje: co jest sprzedażą, co jest aktywnym klientemMS_Description, polecenie („Zasady liczenia”)
Strefy czasowe, waluty, jednostkiPoprawne granice dni i sumyOpisy kolumn (OrderDate lokalnie, PaidAtUtc w UTC)
Rozmiar tabel i istniejące indeksyRozwiązania skalujące się do milionów wierszysys.partitions, sys.indexes
Znane osobliwości danychObsługa NULL i wyjątkówOpis („Country = NULL w kontach ze starego systemu”)
Przykładowe wiersze (zanonimizowane)Format wartości, przykłady do testówPróbka z sekcji o anonimizacji
Czego nie robićBezpieczeństwo: tylko SELECT, bez zmian w danychPolecenie i uprawnienia loginu

JOIN, CTE, podzapytania i funkcje okienkowe z AI

Proste zapytanie AI napisze zawsze. Ciekawie robi się przy konstrukcjach, które sprawiają kłopot także ludziom: złączeniach wielu tabel, podzapytaniach z NOT EXISTS, CTE (czyli nazwanych podzapytaniach w klauzuli WITH), funkcjach okienkowych liczonych „obok” grupowania i zapytaniach rekurencyjnych. Daliśmy ChatGPT sześć takich zadań w jednej rozmowie. Pierwsze polecenie zawierało ten sam wstęp co wcześniej (dialekt, plik ze schematem, zasady liczenia), kolejne już tylko treść zadania. Każdy wynik sprawdziliśmy niezależnym zapytaniem.

Zadanie 1. Klienci bez zamówień: NOT EXISTS

Pierwsze zadanie: liczba klientów zarejestrowanych w 2025 roku, którzy do dziś nie złożyli ani jednego zamówienia, w podziale na miesiąc rejestracji.

ChatGPT pisze zapytanie T-SQL z NOT EXISTS liczące klientów zarejestrowanych w 2025 roku bez żadnego zamówienia, w podziale na miesiąc rejestracji
Pierwsza tura rozmowy: wstęp z zasadami i zadanie. ChatGPT wybiera NOT EXISTS, czyli warunek „nie istnieje żadne zamówienie tego klienta”, i grupuje po miesiącu z CreatedAt.
SELECT
    MONTH(c.CreatedAt) AS RegistrationMonth,
    COUNT(*) AS CustomersWithoutOrders
FROM dbo.Customers AS c
WHERE
    c.CreatedAt >= '20250101'
    AND c.CreatedAt < '20260101'
    AND NOT EXISTS
    (
        SELECT 1
        FROM dbo.Orders AS o
        WHERE o.CustomerID = c.CustomerID
    )
GROUP BY
    MONTH(c.CreatedAt)
ORDER BY
    RegistrationMonth;
Wynik w SSMS: liczba klientów zarejestrowanych w 2025 roku bez zamówień, od 322 w lutym do 404 w sierpniu
Wynik: 12 miesięcy, łącznie 4461 klientów, czyli niecała jedna czwarta z 18 217 kont założonych w 2025 roku. Niezależne zapytanie z NOT IN dało identyczne liczby.

Wybór NOT EXISTS zamiast NOT IN to dobry odruch. Tutaj obie wersje dają ten sam wynik, bo Orders.CustomerID nie dopuszcza NULL. Gdyby dopuszczała i choć jeden wiersz miał NULL, NOT IN zwróciłoby zero wierszy. Ten błąd pokazujemy na żywo w sekcji 20 błędów.

Zadanie 2. Produkty droższe od średniej kategorii: AI dopytuje zamiast zgadywać

Kolejne zadanie: produkty droższe od średniej ceny katalogowej w swojej kategorii. Pokaż nazwę produktu, kategorię, cenę, średnią kategorii i o ile procent produkt jest droższy. Tylko aktywne produkty, 15 największych różnic procentowych.

ChatGPT zamiast pisać zapytanie zadaje pytanie doprecyzowujące: czy średnią kategorii liczyć tylko z aktywnych produktów, czy ze wszystkich
Zamiast kodu pytanie. „Tylko aktywne produkty” można rozumieć na dwa sposoby: średnia liczona z aktywnych albo średnia ze wszystkich, a tylko wynik ograniczony do aktywnych. Te dwie wersje dają różne liczby.

To efekt jednego zdania ze wstępu: „Jeśli do odpowiedzi brakuje Ci informacji, zapytaj, zamiast zgadywać”. Pytanie jest trafne: w 17 kategoriach DemoShop są produkty wycofane, więc obie średnie się różnią, na przykład w kamerach monitoringu 527,15 zł liczone z aktywnych i 487,93 zł liczone ze wszystkich. Odpowiadamy:

Doprecyzowanie do zadania o produktach droższych od średniej: średnią kategorii licz wyłącznie z aktywnych produktów i w wyniku też pokazuj tylko aktywne.

ChatGPT po doprecyzowaniu pisze zapytanie z funkcją okienkową AVG OVER PARTITION BY CategoryID liczącą średnią cenę kategorii z aktywnych produktów
Po doprecyzowaniu: funkcja okienkowa AVG(p.ListPrice) OVER (PARTITION BY p.CategoryID) liczy średnią kategorii przy każdym wierszu, bez grupowania i bez drugiego złączenia z tą samą tabelą.
WITH ActiveProducts AS
(
    SELECT
        p.ProductID,
        p.ProductName,
        p.CategoryID,
        p.ListPrice,
        AVG(p.ListPrice) OVER
        (
            PARTITION BY p.CategoryID
        ) AS CategoryAvgPrice
    FROM dbo.Products AS p
    WHERE p.IsActive = 1
)
SELECT TOP (15)
    ap.ProductName,
    c.CategoryName,
    ap.ListPrice,
    ap.CategoryAvgPrice,
    (ap.ListPrice - ap.CategoryAvgPrice)
        * 100.0
        / ap.CategoryAvgPrice AS MoreExpensivePct
FROM ActiveProducts AS ap
INNER JOIN dbo.Categories AS c
    ON c.CategoryID = ap.CategoryID
WHERE ap.ListPrice > ap.CategoryAvgPrice
ORDER BY
    MoreExpensivePct DESC,
    ap.ProductName;
Wynik w SSMS: 15 aktywnych produktów najbardziej droższych od średniej ceny swojej kategorii, z procentem różnicy
Wynik: na czele zasilacz UPS 1500 VA za 2199 zł przy średniej kategorii 566,34 zł, czyli o 288% drożej. Średnią sprawdziliśmy osobnym zapytaniem z AVG i GROUP BY. Przy drugiej interpretacji polecenia do pierwszej piętnastki trafiłyby te same produkty, ale w innej kolejności.

Funkcja okienkowa to tutaj najczystsze rozwiązanie. Klasyczna alternatywa, czyli podzapytanie skorelowane z AVG dla każdej kategorii albo złączenie z pogrupowaną kopią tabeli, daje ten sam wynik, ale jest dłuższa i łatwiej w niej o pomyłkę w warunku złączenia.

Zadanie 3. Dni między pierwszym a drugim zamówieniem: CTE i ROW_NUMBER

Kolejne zadanie: dla klientów indywidualnych (B2C) policz, ile dni średnio mija między pierwszym a drugim zamówieniem, w podziale na rok pierwszego zamówienia. Użyj CTE.

ChatGPT pisze zapytanie z dwoma CTE: ROW_NUMBER numeruje zamówienia klienta, a agregacja warunkowa wybiera daty pierwszego i drugiego zamówienia
ChatGPT numeruje zamówienia każdego klienta funkcją ROW_NUMBER, w drugim CTE wyciąga daty pierwszego i drugiego zamówienia, a na końcu liczy średnią różnicę dni. Na początku odpowiedzi uczciwie zapisuje przyjęte założenie o zamówieniach anulowanych.
WITH RankedOrders AS
(
    SELECT
        o.CustomerID,
        o.OrderDate,
        ROW_NUMBER() OVER
        (
            PARTITION BY o.CustomerID
            ORDER BY o.OrderDate, o.OrderID
        ) AS rn
    FROM dbo.Orders AS o
    INNER JOIN dbo.Customers AS c
        ON c.CustomerID = o.CustomerID
    WHERE c.Segment = 'B2C'
),
FirstTwoOrders AS
(
    SELECT
        CustomerID,
        MAX(CASE WHEN rn = 1 THEN OrderDate END) AS FirstOrderDate,
        MAX(CASE WHEN rn = 2 THEN OrderDate END) AS SecondOrderDate
    FROM RankedOrders
    WHERE rn <= 2
    GROUP BY CustomerID
)
SELECT
    YEAR(FirstOrderDate) AS FirstOrderYear,
    AVG(
        CAST(
            DATEDIFF(DAY, FirstOrderDate, SecondOrderDate)
            AS decimal(10,2)
        )
    ) AS AvgDaysBetweenOrders
FROM FirstTwoOrders
WHERE SecondOrderDate IS NOT NULL
GROUP BY YEAR(FirstOrderDate)
ORDER BY FirstOrderYear;
Wynik w SSMS: średnia liczba dni między pierwszym a drugim zamówieniem klientów B2C w latach 2023-2026
Wynik: 204,5 dnia dla klientów, którzy zaczęli w 2023 roku, 238,5 dla 2024, 180,0 dla 2025 i tylko 85,2 dla 2026.

Zapytanie jest poprawne, ale wynik wymaga komentarza, którego AI nie dodało. Liczba 85,2 dnia dla 2026 roku nie znaczy, że klienci nagle zaczęli wracać szybciej. Dane kończą się we wrześniu 2026, więc w tej grupie są tylko klienci, którzy zdążyli złożyć drugie zamówienie w ciągu kilku miesięcy. Ci, którzy wrócą później, jeszcze się nie policzyli. Statystycy nazywają to obcięciem obserwacji. AI napisało dobre SQL, ale interpretacja wyniku to wciąż Twoja praca. Warto zawsze dopytać: „Czy któryś okres w wyniku jest niepełny albo nieporównywalny z pozostałymi?”.

Zadanie 4. Trzy najlepsze produkty w każdej kategorii głównej: ranking z remisami

Kolejne zadanie: 3 najlepiej sprzedające się produkty według kwoty sprzedaży w każdej kategorii głównej w 2025 roku. Kategoria główna to kategoria bez rodzica. Przy remisie pokaż oba produkty.

ChatGPT dopytuje o głębokość hierarchii kategorii, bo od niej zależy, czy potrzebne będzie rekurencyjne CTE
Drugie pytanie doprecyzowujące w tej rozmowie: schemat mówi, że kategoria ma rodzica, ale nie mówi, ile poziomów ma hierarchia. Przy dowolnej głębokości potrzebne byłoby zapytanie rekurencyjne.

Doprecyzowanie do zadania o 3 najlepszych produktach: hierarchia kategorii ma dokładnie dwa poziomy - kategoria główna i podkategoria. Każdy produkt jest przypisany do podkategorii.

ChatGPT pisze zapytanie z podwójnym złączeniem tabeli Categories (podkategoria i kategoria główna) i funkcją DENSE_RANK wybierającą trzy najlepsze produkty
Dwa poziomy hierarchii to dwa złączenia tej samej tabeli Categories pod różnymi aliasami: sub (podkategoria produktu) i main (kategoria główna). Ranking liczy DENSE_RANK w obrębie kategorii głównej.
WITH ProductSales AS
(
    SELECT
        main.CategoryID AS MainCategoryID,
        main.CategoryName AS MainCategoryName,
        p.ProductID,
        p.ProductName,
        SUM(
            CAST(oi.Quantity AS decimal(18,2))
            * oi.UnitPrice
            * (100 - oi.DiscountPct) / 100
        ) AS SalesAmount
    FROM dbo.Orders AS o
    INNER JOIN dbo.OrderItems AS oi
        ON oi.OrderID = o.OrderID
    INNER JOIN dbo.Products AS p
        ON p.ProductID = oi.ProductID
    INNER JOIN dbo.Categories AS sub
        ON sub.CategoryID = p.CategoryID
    INNER JOIN dbo.Categories AS main
        ON main.CategoryID = sub.ParentCategoryID
    WHERE
        o.OrderDate >= '20250101'
        AND o.OrderDate < '20260101'
        AND o.Status <> 'CANCELLED'
        AND main.ParentCategoryID IS NULL
    GROUP BY
        main.CategoryID,
        main.CategoryName,
        p.ProductID,
        p.ProductName
),
RankedProducts AS
(
    SELECT
        MainCategoryID,
        MainCategoryName,
        ProductID,
        ProductName,
        SalesAmount,
        DENSE_RANK() OVER
        (
            PARTITION BY MainCategoryID
            ORDER BY SalesAmount DESC
        ) AS SalesRank
    FROM ProductSales
)
SELECT
    MainCategoryName,
    ProductName,
    SalesAmount,
    SalesRank
FROM RankedProducts
WHERE SalesRank <= 3
ORDER BY
    MainCategoryName,
    SalesRank,
    ProductName;
Wynik w SSMS: trzy najlepiej sprzedające się produkty 2025 roku w każdej z ośmiu kategorii głównych DemoShop
24 wiersze: po trzy produkty w każdej z ośmiu kategorii głównych. W Audio prowadzą słuchawki dokanałowe SL-565 z 10,2 mln zł sprzedaży.

Tu warto zatrzymać się przy jednym słowie. AI użyło DENSE_RANK, a nie RANK. Przy remisie na pierwszym miejscu DENSE_RANK nada kolejnemu produktowi numer 2, więc w wyniku mogą się znaleźć cztery produkty z kategorii. RANK przeskoczy do numeru 3 i zwróci trzy. W naszych danych remisów nie ma i obie wersje dają te same 24 wiersze, ale na innych danych różnica wyjdzie. Jeśli ranking ma znaczenie biznesowe (premie, wyróżnienia), dopytaj AI wprost: „Czym różni się tu RANK, DENSE_RANK i ROW_NUMBER i którą wybrałeś?”.

Zadanie 5. Sprzedaż narastająco i zmiana miesiąc do miesiąca: SUM OVER i LAG

Kolejne zadanie: sprzedaż miesięczna w 2025 roku. Kwota w miesiącu, suma narastająco od początku roku i zmiana procentowa względem poprzedniego miesiąca.

ChatGPT pisze zapytanie ze sprzedażą miesięczną, sumą narastającą SUM OVER z ramką ROWS i funkcją LAG do zmiany procentowej
Suma narastająco z jawnie podaną ramką ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, poprzedni miesiąc z funkcji LAG i ochrona przed dzieleniem przez zero.
WITH MonthlySales AS
(
    SELECT
        DATEFROMPARTS(YEAR(o.OrderDate), MONTH(o.OrderDate), 1) AS SalesMonth,
        SUM(
            CAST(oi.Quantity AS decimal(18,2))
            * oi.UnitPrice
            * (100 - oi.DiscountPct) / 100
        ) AS MonthlyAmount
    FROM dbo.Orders AS o
    INNER JOIN dbo.OrderItems AS oi
        ON oi.OrderID = o.OrderID
    WHERE
        o.OrderDate >= '20250101'
        AND o.OrderDate < '20260101'
        AND o.Status <> 'CANCELLED'
    GROUP BY
        DATEFROMPARTS(YEAR(o.OrderDate), MONTH(o.OrderDate), 1)
),
WithPrevious AS
(
    SELECT
        SalesMonth,
        MonthlyAmount,
        SUM(MonthlyAmount) OVER
        (
            ORDER BY SalesMonth
            ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
        ) AS YtdAmount,
        LAG(MonthlyAmount) OVER
        (
            ORDER BY SalesMonth
        ) AS PreviousMonthAmount
    FROM MonthlySales
)
SELECT
    SalesMonth,
    MonthlyAmount,
    YtdAmount,
    CASE
        WHEN PreviousMonthAmount IS NULL
             OR PreviousMonthAmount = 0
            THEN NULL
        ELSE
            (MonthlyAmount - PreviousMonthAmount)
            * 100.0
            / PreviousMonthAmount
    END AS MoMChangePct
FROM WithPrevious
ORDER BY SalesMonth;
Wynik w SSMS: sprzedaż w każdym miesiącu 2025 roku, suma narastająco i procentowa zmiana względem poprzedniego miesiąca
Od 146,9 mln zł w styczniu do 318,9 mln zł w grudniu. Suma narastająca na koniec roku to 2 283 093 002,77 zł, tyle samo co w zapytaniu o województwa. Listopad rośnie o 29,9%, grudzień o kolejne 15,9%.

Na uwagę zasługuje ramka ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. Gdyby jej nie było, SQL Server przyjąłby domyślnie ramkę RANGE, która przy powtarzających się wartościach w ORDER BY dolicza cały „remis” naraz. Tutaj miesiące się nie powtarzają, więc wynik byłby ten sam, ale przy sumie narastającej po dniu albo po kwocie już nie. Różnicę na danych pokazujemy w błędzie 17 z listy dwudziestu typowych błędów.

Zadanie 6. Struktura organizacyjna: rekurencyjne CTE

Ostatnie zadanie: struktura organizacyjna z tabeli dbo.Employees jako drzewo. Poziom w hierarchii, imię i nazwisko, stanowisko i pełna ścieżka przełożonych od prezesa.

ChatGPT pisze rekurencyjne CTE budujące drzewo pracowników od prezesa, z poziomem hierarchii i ścieżką przełożonych
Rekurencyjne CTE: część zakotwiczająca bierze prezesa (ManagerID IS NULL), część rekurencyjna dokleja kolejnych podwładnych i wydłuża ścieżkę. Na końcu OPTION (MAXRECURSION 100) jako bezpiecznik.
WITH EmployeeTree AS
(
    -- Korzeń hierarchii
    SELECT
        e.EmployeeID,
        e.ManagerID,
        e.FirstName,
        e.LastName,
        e.JobTitle,
        0 AS HierarchyLevel,
        CAST(
            e.FirstName + N' ' + e.LastName
            AS nvarchar(max)
        ) AS ManagerPath
    FROM dbo.Employees AS e
    WHERE e.ManagerID IS NULL

    UNION ALL

    -- Kolejne poziomy
    SELECT
        e.EmployeeID,
        e.ManagerID,
        e.FirstName,
        e.LastName,
        e.JobTitle,
        et.HierarchyLevel + 1,
        CAST(
            et.ManagerPath + N' > '
            + e.FirstName + N' ' + e.LastName
            AS nvarchar(max)
        ) AS ManagerPath
    FROM dbo.Employees AS e
    INNER JOIN EmployeeTree AS et
        ON e.ManagerID = et.EmployeeID
)
SELECT
    HierarchyLevel,
    FirstName + N' ' + LastName AS EmployeeName,
    JobTitle,
    ManagerPath
FROM EmployeeTree
ORDER BY
    ManagerPath
OPTION (MAXRECURSION 100);
Wynik rekurencyjnego CTE w SSMS: 48 pracowników z poziomem hierarchii, stanowiskiem i pełną ścieżką przełożonych
48 pracowników: prezes na poziomie 0, trzech dyrektorów na poziomie 1, czterech kierowników na poziomie 2 i 40 osób na poziomie 3, każda z pełną ścieżką przełożonych.

Po sześciu zadaniach widać wzorzec. Z dobrym kontekstem AI pisze poprawne i nowoczesne SQL: funkcje okienkowe zamiast samozłączeń, zakresy dat zamiast funkcji na kolumnach, jawne ramki okien. Dwa razy zadało pytanie, zamiast zgadywać. Słabym punktem nie była składnia, tylko to, czego w poleceniu nie było: interpretacja niepełnego roku i świadomy wybór między rodzajami rankingu.

✕Powiększony zrzut ekranu z przewodnika AI w SQL
Szkolenie Język SQL ze wsparciem AI

Szkolenie Język SQL ze wsparciem AI

Najbliższy termin gwarantowany: 28.10.2026

Sprawdź terminy

Najczęściej zadawane pytania

Dlaczego ChatGPT wymyśla nazwy tabel i kolumn w zapytaniach SQL?
Model nie zna Twojej bazy, więc uzupełnia brakujące informacje najbardziej prawdopodobnymi nazwami z danych treningowych. Pomaga załączenie opisu schematu, zakaz używania obiektów spoza niego i zgoda na odpowiedź, że czegoś nie da się policzyć.
Jak przygotować opis schematu bazy SQL Server dla AI?
Najwygodniej jednym zapytaniem do widoków systemowych, które wypisuje tabele z liczbą wierszy, kolumny z typami, klucze, relacje i opisy kolumn. Wynik kopiujesz w trybie tekstowym i zapisujesz do pliku .txt, który załączasz do rozmowy.
Jak wymusić na AI konkretny dialekt SQL?
Podaj silnik i wersję w pierwszym zdaniu polecenia, na przykład SQL Server 2022 z poziomem zgodności 160. Bez tego model często wybiera składnię MySQL albo PostgreSQL, na przykład LIMIT zamiast TOP, co kończy się błędem składni.
Jak sprawdzić, czy zapytanie od ChatGPT liczy poprawnie?
Uruchom osobne, prostsze zapytanie, które liczy to samo inną drogą, i porównaj wyniki co do grosza. Kontrola musi sprawdzać ten sam obiekt: identyfikator klienta, a nie imię i nazwisko, które mogą się powtarzać.

Komentarze (0)

Musisz być zalogowany by móc dodać komentarz. Zaloguj się przez Google

Brak komentarzy...