Blog JSystems - uwalniamy wiedzę!

Szukaj
Błędy SQL z AI: zrzuty z ChatGPT i SQL Server Management Studio z części 3 przewodnika AI w SQL
Część 3 przewodnika AI w SQL. Na obrazie trzy prawdziwe zrzuty z tej części.

W skrócieBłąd składniowy AI znajduje z komunikatu serwera, a błąd logiczny dopiero wtedy, gdy dostaje cel zapytania i liczbę odniesienia. Pokazujemy osiem błędów składni, studium raportu, który zawyżał przychód o 15,6%, i 20 typowych błędów logicznych z wynikami przed poprawką i po niej.

To część 3 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.

Wyszukiwanie błędów składniowych z AI

Błąd składniowy to sytuacja, w której serwer w ogóle nie rozumie zapytania albo nie może go wykonać, więc kończy je komunikatem. To najłatwiejsza kategoria błędów, bo serwer sam wskazuje problem. Kłopot w tym, że komunikat bywa lakoniczny, po angielsku i wskazuje miejsce, w którym parser się poddał, a nie miejsce, w którym naprawdę jest błąd. AI świetnie tłumaczy takie komunikaty, pod warunkiem że dostanie trzy rzeczy: dialekt i wersję, dokładną treść komunikatu z numerem błędu i całe zapytanie.

SQL Server 2022 zwraca błąd [numer]: [treść komunikatu skopiowana z zakładki Komunikaty].
Co jest nie tak i jak to poprawić?

[całe zapytanie]

Przygotowaliśmy osiem zapytań z najczęstszymi błędami, uruchomiliśmy je w SSMS i każdy komunikat w dokładnie takiej postaci przekazaliśmy ChatGPT. Poniżej wszystkie osiem przypadków: najpierw to, co pokazał serwer, potem odpowiedź AI.

NrBłądKomunikat SQL ServerPrzyczyna
1Brak przecinkabrak, zapytanie działadruga kolumna stała się aliasem pierwszej
2Niedomknięty nawias156: Incorrect syntax near the keyword 'GROUP'parser dotarł do GROUP BY z otwartym nawiasem
3Alias w WHERE207: Invalid column name 'LineValue'WHERE działa przed SELECT, alias jeszcze nie istnieje
4Nieistniejąca kolumna207: Invalid column name 'TotalAmount'kolumny nie ma w tabeli, wartość trzeba policzyć
5Funkcja z innego dialektu195: 'DATE_TRUNC' is not a recognized built-in function nameDATE_TRUNC jest w PostgreSQL, w SQL Server 2022 jest DATETRUNC
6GROUP BY8120: Column 'dbo.Customers.Segment' is invalid in the select list...kolumna w SELECT bez agregacji i bez GROUP BY
7Konwersja typów245: Conversion failed when converting the varchar value 'B2B-000001' to data type intporównanie tekstu z liczbą
8Składnia innego dialektu102: Incorrect syntax near 'LIMIT'LIMIT nie istnieje w T-SQL

1. Brak przecinka: błąd, którego serwer nie zgłosi

SELECT TOP (5) CustomerID, FirstName LastName, City
FROM dbo.Customers
ORDER BY CustomerID;
SSMS wykonuje zapytanie z brakującym przecinkiem bez błędu, a w kolumnie LastName pokazuje imiona klientów
Zapytanie wykonuje się bez błędu. Kolumna nazywa się LastName, ale zawiera imiona: Wojciech, Szymon, Igor. Brakujący przecinek zamienił drugą kolumnę w alias pierwszej.

Najgroźniejszy błąd na tej liście, bo nie ma komunikatu. W T-SQL słowo AS przed aliasem jest opcjonalne, więc FirstName LastName znaczy „kolumna FirstName nazwana LastName”. Tu łatwo to zauważyć, ale w zapytaniu z trzydziestoma kolumnami jedna zgubiona kolumna przejdzie niezauważona. Opisujemy objaw, a nie komunikat:

ChatGPT wyjaśnia, że brak przecinka między FirstName i LastName sprawia, że SQL Server traktuje LastName jako alias kolumny FirstName
ChatGPT od razu wskazuje przyczynę i poprawioną listę kolumn. Dla ochrony przed tym błędem warto zawsze pisać aliasy ze słowem AS. Wtedy każde gołe słowo po kolumnie od razu rzuca się w oczy.

2. Niedomknięty nawias: błąd zgłoszony w złym miejscu

SSMS zgłasza błąd 156 Incorrect syntax near the keyword GROUP dla zapytania z niedomkniętym nawiasem w klauzuli WHERE
Komunikat 156 wskazuje słowo GROUP, ale błąd jest wyżej: nawias otwarty przed Status nigdy się nie zamyka. Parser czytał dalej, aż trafił na GROUP BY w miejscu, w którym spodziewał się nawiasu.
ChatGPT wskazuje brakujący nawias zamykający w klauzuli WHERE i proponuje wersję z operatorem IN
ChatGPT zamyka nawias po drugim statusie, a nie na końcu warunku, i podaje czytelniejszą wersję z IN. To ważniejsze, niż wygląda.

Miejsce zamknięcia nawiasu zmienia znaczenie zapytania. Operator AND wiąże silniej niż OR, więc nawias zamknięty na końcu całego warunku dałby „wszystkie zamówienia doręczone z dowolnego roku plus zwrócone z 2025”. AI wybrało wersję zgodną z sensem („jeden z dwóch statusów, od 2025 roku”) i napisało to wprost. Tę samą pułapkę bez nawiasów, czyli w wersji, która wykonuje się bez błędu i zwraca trzy razy za dużo wierszy, pokazujemy jako błąd 19 na liście dwudziestu typowych błędów.

3. Alias w WHERE

SSMS zwraca błąd 207 Invalid column name LineValue, bo alias zdefiniowany w SELECT użyto w klauzuli WHERE
Błąd 207 dla kolumny LineValue, którą zapytanie samo definiuje w SELECT. Logicznie WHERE wykonuje się wcześniej, więc aliasu jeszcze nie ma.
ChatGPT wyjaśnia, że alias LineValue nie jest dostępny w WHERE, i podaje dwie poprawki: powtórzenie wyrażenia albo podzapytanie
Dwie poprawki: powtórzenie wyrażenia w WHERE albo podzapytanie (lub CTE), w którym alias już istnieje. Na końcu przypomnienie, że w ORDER BY alias działa, bo ORDER BY wykonuje się po SELECT.

4. Nieistniejąca kolumna

SSMS zwraca błąd 207 Invalid column name TotalAmount dla kolumny, której nie ma w tabeli dbo.Orders
Błąd 207 dla TotalAmount. Kolumny o tej nazwie w DemoShop nie ma, a wartość zamówienia trzeba policzyć z pozycji.
ChatGPT radzi sprawdzić nazwy kolumn przez sp_help i proponuje wyliczenie wartości zamówienia z tabeli OrderItems
ChatGPT nie zgaduje, jak nazywa się właściwa kolumna. Podpowiada, jak sprawdzić prawdziwe nazwy (sp_help, SELECT TOP (1) *), i pokazuje, jak policzyć sumę z OrderItems, jeśli wartości w tabeli zamówień nie ma.

To przykład, w którym AI bez schematu nie może dać ostatecznej odpowiedzi, i dobrze, że tego nie udaje. Jeśli chcesz od razu poprawny kod, dołącz opis schematu z sekcji o schemacie.

5. Funkcja z innego dialektu

SSMS zwraca błąd 195 DATE_TRUNC is not a recognized built-in function name dla funkcji z PostgreSQL
DATE_TRUNC to funkcja PostgreSQL. SQL Server zgłasza błąd 195: nierozpoznana funkcja wbudowana.
ChatGPT wyjaśnia, że w SQL Server 2022 odpowiednikiem DATE_TRUNC jest DATETRUNC, a część daty podaje się bez apostrofów
Odpowiedź wskazuje dwa problemy naraz: nazwę funkcji (DATETRUNC zamiast DATE_TRUNC) i sposób podania części daty (month bez apostrofów). Dodaje, że DATETRUNC pojawiło się dopiero w SQL Server 2022.

Ostatnia uwaga z odpowiedzi jest praktyczna: na SQL Server 2019 DATETRUNC nie istnieje i poprawka też skończyłaby się błędem 195. Wtedy pierwszy dzień miesiąca liczy się na przykład przez DATEFROMPARTS(YEAR(OrderDate), MONTH(OrderDate), 1).

6. Kolumna spoza GROUP BY

SSMS zwraca błąd 8120: kolumna dbo.Customers.Segment jest na liście SELECT, ale nie ma jej w GROUP BY ani w funkcji agregującej
Błąd 8120: Segment jest w SELECT, ale grupujemy tylko po City. Serwer nie wie, który segment pokazać dla miasta, w którym mieszkają klienci obu segmentów.
ChatGPT wyjaśnia błąd 8120 i podaje dwie poprawki: dodanie Segment do GROUP BY albo usunięcie go z SELECT, w zależności od celu
AI nie wybiera za Ciebie. Daje dwie poprawne wersje o różnym znaczeniu: liczbę zamówień dla każdej pary miasto i segment albo tylko dla miasta.

7. Konwersja typów

SSMS zwraca błąd 245 Conversion failed when converting the varchar value B2B-000001 to data type int
Błąd 245. CustomerCode to tekst, a porównujemy go z liczbą, więc SQL Server próbuje zamienić każdy kod na liczbę. Kody klientów firmowych z prefiksem B2B- się nie dają.
ChatGPT wyjaśnia, że kolumnę tekstową trzeba porównywać z wartością w apostrofach, i podaje poprawione zapytania dla kodu liczbowego i kodu B2B
Poprawka jest prosta: wartość w apostrofach. ChatGPT dodaje zdanie, które warto zapamiętać: inaczej SQL Server może próbować konwertować całą kolumnę do typu liczbowego.

To zdanie ma też drugie dno, o którym AI nie wspomniało. Konwersja całej kolumny oznacza, że serwer nie może skorzystać z indeksu na CustomerCode i musi przeczytać wszystkie kody. Gdyby w tabeli nie było kodów z literami, zapytanie wykonałoby się bez błędu, tylko wolniej. Taką cichą konwersję pokazujemy w sygnale nr 9 w sekcji o planie wykonania.

8. Składnia z innego dialektu

SSMS zwraca błąd 102 Incorrect syntax near LIMIT, bo klauzula LIMIT nie istnieje w T-SQL
Błąd 102 przy LIMIT. W T-SQL liczbę wierszy ogranicza TOP albo OFFSET ... FETCH.
ChatGPT zamienia LIMIT 10 na TOP (10) i pokazuje alternatywę OFFSET FETCH w SQL Server
Dwie poprawne wersje: TOP (10) oraz OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY, która przydaje się przy stronicowaniu wyników.

Osiem na osiem trafnych diagnoz, każda w 7-12 sekund. Przy błędach składniowych AI jest po prostu szybsze od szukania w dokumentacji. Dwie praktyczne rady z tych prób: wklejaj komunikat dokładnie tak, jak go podał serwer (z numerem, bo po numerze AI rozpoznaje kategorię błędu), a przy błędzie bez komunikatu, jak w pierwszym przypadku, opisz objaw („w kolumnie LastName widzę imiona”) zamiast pytać ogólnie, co jest nie tak.

Błędy logiczne: zapytanie działa, ale wynik się nie zgadza

Błąd logiczny to sytuacja, w której zapytanie wykonuje się bez komunikatu i zwraca wiarygodnie wyglądające liczby, tylko złe. Serwer nic nie podpowie, więc trzeba wiedzieć, że coś jest nie tak, i znaleźć przyczynę. AI pomaga tu na dwa sposoby. Pierwszy to przegląd kodu pod kątem celu: dajesz cel i zapytanie, AI szuka niezgodności. Tak sprawdziliśmy 20 typowych błędów w następnej sekcji. Drugi sposób jest potrzebny, gdy błąd nie rzuca się w oczy, a jedyny sygnał to liczba, która nie zgadza się z inną liczbą. Wtedy AI jest najlepszym partnerem do stawiania hipotez i projektowania zapytań diagnostycznych.

Metoda w pięciu krokach

  1. Ustal liczbę odniesienia. Bez niej nie wiesz, czy wynik jest zły. To może być system księgowy, inny raport, ręczne policzenie kilku przypadków albo wynik niezależnego, prostszego zapytania.
  2. Daj AI cel, objaw i liczby, ale poproś o hipotezy, nie o poprawkę. Model, który od razu poprawia, wybiera jedną przyczynę i ją „naprawia”. Model, który ma wypisać hipotezy od najbardziej prawdopodobnej, rozważa kilka.
  3. Uruchom zapytania diagnostyczne na bazie. Hipoteza bez sprawdzenia na danych to wciąż zgadywanie.
  4. Zwróć AI wyniki diagnostyki i reguły biznesowe, których brakowało, i dopiero teraz poproś o poprawkę i o sposób jej weryfikacji.
  5. Zweryfikuj poprawkę niezależnie. Liczba po poprawce musi zgodzić się z liczbą odniesienia, a różnica między starą i nową wersją musi być w całości wyjaśniona.

Studium przypadku: raport zawyża przychód o 15,6%

Dział finansów dostaje co miesiąc raport przychodu z opłaconych zamówień. Zapytanie wygląda niewinnie: zamówienia z 2025 roku, złączone z udanymi płatnościami (żeby wziąć tylko opłacone) i z pozycjami (żeby policzyć wartość).

/* Raport: przychód z opłaconych zamówień w 2025 roku, miesiąc po miesiącu */
SELECT DATEFROMPARTS(YEAR(o.OrderDate), MONTH(o.OrderDate), 1) AS SalesMonth,
       SUM(oi.Quantity * oi.UnitPrice * (100 - oi.DiscountPct) / 100) AS PaidRevenue
FROM dbo.Orders AS o
JOIN dbo.Payments AS p ON p.OrderID = o.OrderID
                      AND p.Status = 'SUCCESS'
JOIN dbo.OrderItems AS oi ON oi.OrderID = o.OrderID
WHERE o.OrderDate >= '20250101' AND o.OrderDate < '20260101'
GROUP BY DATEFROMPARTS(YEAR(o.OrderDate), MONTH(o.OrderDate), 1)
ORDER BY SalesMonth;
Raport przychodu z opłaconych zamówień w SSMS: dwanaście miesięcy 2025 roku z kwotami od 148,7 do 368,8 mln zł
Krok 1. Raport działa i zwraca 12 miesięcy. Nic w nim nie wygląda podejrzanie, dopóki ktoś nie porówna sumy z księgowością.

Finanse zgłaszają, że suma roczna nie zgadza się z systemem księgowym. Sprawdzamy to jednym zapytaniem, które sumuje raport i zestawia go z kwotą z księgowości:

/* Suma roczna raportu a kwota z księgowości */
WITH Raport AS (
    SELECT DATEFROMPARTS(YEAR(o.OrderDate), MONTH(o.OrderDate), 1) AS SalesMonth,
           SUM(oi.Quantity * oi.UnitPrice * (100 - oi.DiscountPct) / 100) AS PaidRevenue
    FROM dbo.Orders AS o
    JOIN dbo.Payments AS p ON p.OrderID = o.OrderID AND p.Status = 'SUCCESS'
    JOIN dbo.OrderItems AS oi ON oi.OrderID = o.OrderID
    WHERE o.OrderDate >= '20250101' AND o.OrderDate < '20260101'
    GROUP BY DATEFROMPARTS(YEAR(o.OrderDate), MONTH(o.OrderDate), 1)
)
SELECT SUM(PaidRevenue) AS ReportTotal,
       CAST(2283093002.77 AS decimal(19, 2)) AS AccountingTotal,
       CAST((SUM(PaidRevenue) / 2283093002.77 - 1) * 100 AS decimal(5, 2)) AS DifferencePct
FROM Raport;
Porównanie w SSMS: suma roczna raportu 2 638 350 266,68 zł wobec 2 283 093 002,77 zł w księgowości, różnica 15,56%
Krok 2. Liczba odniesienia: raport pokazuje 2 638 350 266,68 zł, księgowość 2 283 093 002,77 zł. Różnica 15,56%, w poszczególnych miesiącach od 15,1% do 16,7%.

Teraz rozmowa z AI. Załączamy opis schematu, podajemy cel raportu, objaw i obie liczby, a zamiast „popraw to” prosimy o hipotezy z zapytaniami diagnostycznymi:

Pracuję w SQL Server 2022 na bazie opisanej w załączonym pliku demoshop_schemat_dla_ai.txt.

Poniższy raport ma pokazywać przychód z opłaconych zamówień w 2025 roku, miesiąc po miesiącu. Dział finansów zgłasza, że suma za rok jest o około 15,6% wyższa niż w systemie księgowym (raport: 2 638 350 266,68 zł, księgowość: 2 283 093 002,77 zł). Różnica jest podobna w każdym miesiącu.

Nie poprawiaj jeszcze zapytania. Wypisz hipotezy, co może zawyżać wynik, od najbardziej prawdopodobnej, i do każdej podaj krótkie zapytanie diagnostyczne, którym ją sprawdzę.

[treść raportu]

ChatGPT wypisuje pięć hipotez zawyżenia raportu, od wielokrotnych płatności SUCCESS po strefę czasową, z zapytaniem diagnostycznym do każdej
Krok 3. Początek odpowiedzi z pierwszą hipotezą i zapytaniem diagnostycznym. Wszystkie pięć hipotez to: wiele udanych płatności na zamówienie, liczenie pełnej wartości przy płatności ratalnej, nieodjęte zwroty, zamówienia później anulowane lub zwrócone i różnica dat między zamówieniem a płatnością.

Ranking hipotez jest trafny. Na pierwszym miejscu ChatGPT stawia złączenie z płatnościami bez sprowadzenia ich do jednego wiersza na zamówienie i uzasadnia to opisem tabeli Payments w schemacie („zamówienie może mieć kilka wierszy: nieudane próby, raty i zwroty”) oraz tym, że płatności jest więcej niż zamówień. Przy hipotezie o datach sam zaznacza, że nie tłumaczy ona stałej różnicy w każdym miesiącu. Uruchamiamy jego zapytania diagnostyczne bez żadnych zmian:

SELECT
    COUNT(*) AS OrdersWithMultipleSuccess,
    SUM(SuccessCount - 1) AS ExtraSuccessRows
FROM (
    SELECT OrderID, COUNT(*) AS SuccessCount
    FROM dbo.Payments
    WHERE Status = 'SUCCESS'
    GROUP BY OrderID
    HAVING COUNT(*) > 1
) AS x;
Diagnostyka w SSMS: 109 013 zamówień w całej bazie ma więcej niż jedną udaną płatność
Krok 4a. W całej bazie 109 013 zamówień ma więcej niż jedną płatność SUCCESS, każde dokładnie dwie.
SELECT
    p.SuccessCount,
    COUNT(*) AS OrdersCount
FROM dbo.Orders AS o
JOIN (
    SELECT OrderID, COUNT(*) AS SuccessCount
    FROM dbo.Payments
    WHERE Status = 'SUCCESS'
    GROUP BY OrderID
) AS p ON p.OrderID = o.OrderID
WHERE o.OrderDate >= '20250101'
  AND o.OrderDate <  '20260101'
GROUP BY p.SuccessCount
ORDER BY p.SuccessCount;
Diagnostyka w SSMS: w 2025 roku 687 719 zamówień ma jedną udaną płatność, a 35 702 zamówienia mają dwie
Krok 4b. W 2025 roku: 687 719 zamówień z jedną płatnością i 35 702 z dwiema. To one są podejrzanymi.
SELECT TOP (100)
    o.OrderID,
    SUM(oi.Quantity * oi.UnitPrice * (100 - oi.DiscountPct) / 100) AS OrderValue,
    p.SuccessCount,
    p.SuccessAmount
FROM dbo.Orders AS o
JOIN dbo.OrderItems AS oi ON oi.OrderID = o.OrderID
JOIN (
    SELECT
        OrderID,
        COUNT(*) AS SuccessCount,
        SUM(Amount) AS SuccessAmount
    FROM dbo.Payments
    WHERE Status = 'SUCCESS'
    GROUP BY OrderID
) AS p ON p.OrderID = o.OrderID
WHERE o.OrderDate >= '20250101'
  AND o.OrderDate <  '20260101'
GROUP BY o.OrderID, p.SuccessCount, p.SuccessAmount
HAVING p.SuccessCount > 1
ORDER BY p.SuccessCount DESC;
Diagnostyka w SSMS: dla zamówień z dwiema płatnościami suma rat równa się wartości zamówienia
Krok 4c. Zamówienia z dwiema płatnościami to raty: suma obu wpłat równa się wartości zamówienia (z dokładnością do zaokrąglenia raty do grosza). Raport liczył więc pozycje takiego zamówienia dwa razy.
SELECT
    COUNT(DISTINCT OrderID) AS OrdersWithRefund,
    -SUM(Amount) AS RefundedAmount
FROM dbo.Payments
WHERE Status = 'REFUNDED'
  AND OrderID IN (
      SELECT OrderID
      FROM dbo.Orders
      WHERE OrderDate >= '20250101'
        AND OrderDate <  '20260101'
  );
Diagnostyka w SSMS: 34 821 zamówień z 2025 roku ma zwrot pieniędzy na łączną kwotę 25 708 369,94 zł
Krok 4d. Zwroty pieniędzy w zamówieniach z 2025 roku: 34 821 zamówień, 25 708 369,94 zł. To prawdziwa kwota, ale za mała, żeby wyjaśnić 355 mln zł różnicy.
SELECT
    o.Status,
    COUNT(DISTINCT o.OrderID) AS OrdersCount
FROM dbo.Orders AS o
JOIN dbo.Payments AS p
  ON p.OrderID = o.OrderID
 AND p.Status = 'SUCCESS'
WHERE o.OrderDate >= '20250101'
  AND o.OrderDate <  '20260101'
GROUP BY o.Status
ORDER BY OrdersCount DESC;
Diagnostyka w SSMS: zamówienia z udaną płatnością w 2025 roku mają status DELIVERED albo RETURNED
Krok 4e. Zamówienia z udaną płatnością mają status DELIVERED (696 926) albo RETURNED (26 495). Żadne nie jest anulowane, więc hipoteza nr 4 odpada.

Wyniki wskazują jednoznacznie pierwszą hipotezę. Zanim poprosimy o poprawkę, dokładamy informację, której AI nie mogło znać: jak liczy księgowość. To rozstrzyga hipotezy nr 3 i nr 5, które dotyczą reguł biznesowych, a nie kodu.

Wyniki diagnostyki:
1. W całej bazie 109 013 zamówień ma więcej niż jedną płatność SUCCESS (ExtraSuccessRows: 109 013, czyli każde ma dokładnie dwie).
2. Zamówienia z 2025 roku: 687 719 ma jedną płatność SUCCESS, 35 702 ma dwie.
3. W zamówieniach z dwiema płatnościami suma rat jest równa wartości zamówienia z dokładnością do grosza, np. zamówienie 1051092: wartość 7335,9604, raty łącznie 7335,96.
4. Zwroty REFUNDED w zamówieniach z 2025 roku: 34 821 zamówień, 25 708 369,94 zł.
5. Zamówienia z udaną płatnością mają status DELIVERED (696 926) albo RETURNED (26 495), żadne nie jest CANCELLED.

Księgowość ujmuje przychód brutto w miesiącu złożenia zamówienia, a zwroty księguje osobno.

Napisz poprawioną wersję raportu. Dodaj też zapytanie, które sprawdzi, czy różnica między starym a nowym raportem jest w całości wyjaśniona przez zamówienia z dwiema płatnościami.

ChatGPT zastępuje złączenie z płatnościami warunkiem EXISTS i dopisuje zapytanie kontrolne porównujące stary i nowy raport miesiąc po miesiącu
Krok 5. Poprawka: zamiast złączenia z płatnościami warunek EXISTS, bo płatność ma tylko zakwalifikować zamówienie, a nie dostarczać kolumn. ChatGPT celowo nie wyklucza zamówień zwróconych i uzasadnia to regułą księgowości: wykluczenie RETURNED odjęłoby zwrot drugi raz.
/* Przychód brutto z opłaconych zamówień w 2025 roku,
   miesiąc po miesiącu.
   Zwroty są księgowane osobno. */

SELECT
    DATEFROMPARTS(YEAR(o.OrderDate), MONTH(o.OrderDate), 1) AS SalesMonth,
    SUM(
        oi.Quantity * oi.UnitPrice * (100 - oi.DiscountPct) / 100
    ) AS PaidRevenue
FROM dbo.Orders AS o
JOIN dbo.OrderItems AS oi
    ON oi.OrderID = o.OrderID
WHERE o.OrderDate >= '20250101'
  AND o.OrderDate <  '20260101'
  AND EXISTS (
      SELECT 1
      FROM dbo.Payments AS p
      WHERE p.OrderID = o.OrderID
        AND p.Status = 'SUCCESS'
  )
GROUP BY
    DATEFROMPARTS(YEAR(o.OrderDate), MONTH(o.OrderDate), 1)
ORDER BY
    SalesMonth;
Poprawiony raport w SSMS: przychód z opłaconych zamówień 2025 roku miesiąc po miesiącu, liczony przez EXISTS zamiast złączenia z płatnościami
Krok 6. Poprawiony raport: każde zamówienie liczy się raz, niezależnie od liczby płatności. Suma dwunastu miesięcy, policzona osobnym zapytaniem, to 2 283 093 002,77 zł, dokładnie tyle, ile pokazuje księgowość.

Pułapka w zapytaniu kontrolnym

Druga część odpowiedzi to zapytanie, które ma udowodnić, że cała różnica pochodzi z drugich płatności. Uruchomione daje zero niewyjaśnionej różnicy w każdym miesiącu. Wygląda to jak dowód, ale nim nie jest. Zapytanie nie uruchamia starego raportu, tylko odtwarza go wzorem „wartość zamówienia razy liczba płatności”. Różnica między takim „starym raportem” a nowym jest wtedy z definicji równa wartości drugich płatności, więc kolumna UnexplainedDifference wyjdzie zerowa dla każdych danych, nawet gdyby stary raport zawyżało coś jeszcze.

WITH PaymentCounts AS (
    SELECT
        p.OrderID,
        COUNT(*) AS SuccessCount
    FROM dbo.Payments AS p
    WHERE p.Status = 'SUCCESS'
    GROUP BY p.OrderID
),
OrderValues AS (
    SELECT
        o.OrderID,
        DATEFROMPARTS(YEAR(o.OrderDate), MONTH(o.OrderDate), 1) AS SalesMonth,
        SUM(
            oi.Quantity * oi.UnitPrice * (100 - oi.DiscountPct) / 100
        ) AS OrderValue
    FROM dbo.Orders AS o
    JOIN dbo.OrderItems AS oi
        ON oi.OrderID = o.OrderID
    WHERE o.OrderDate >= '20250101'
      AND o.OrderDate <  '20260101'
    GROUP BY
        o.OrderID,
        DATEFROMPARTS(YEAR(o.OrderDate), MONTH(o.OrderDate), 1)
),
CheckDiff AS (
    SELECT
        ov.SalesMonth,

        /* Tak zachowywał się stary JOIN:
           wartość zamówienia * liczba SUCCESS */
        SUM(ov.OrderValue * pc.SuccessCount) AS OldRevenue,

        /* Poprawny wynik: każde opłacone zamówienie raz */
        SUM(ov.OrderValue) AS NewRevenue,

        /* Nadwyżka przewidywana wyłącznie przez dodatkowy SUCCESS */
        SUM(ov.OrderValue * (pc.SuccessCount - 1)) AS
            ExpectedDuplicatePaymentOverstatement
    FROM OrderValues AS ov
    JOIN PaymentCounts AS pc
        ON pc.OrderID = ov.OrderID
    GROUP BY ov.SalesMonth
)
SELECT
    SalesMonth,
    OldRevenue,
    NewRevenue,
    OldRevenue - NewRevenue AS ActualDifference,
    ExpectedDuplicatePaymentOverstatement,
    (OldRevenue - NewRevenue)
        - ExpectedDuplicatePaymentOverstatement AS UnexplainedDifference
FROM CheckDiff
ORDER BY SalesMonth;
Zapytanie kontrolne od AI w SSMS zwraca zero niewyjaśnionej różnicy w każdym miesiącu, bo odtwarza stary raport wzorem zamiast go uruchomić
Kontrola od AI: zera we wszystkich miesiącach. Problem w tym, że to zapytanie dałoby zera zawsze, bo liczy obie strony z tego samego założenia.

Niezależna kontrola musi porównać rzeczywiste wyniki obu raportów z czymś, co policzono inną drogą. Wstawiamy oba raporty bez zmian do CTE i odejmujemy od ich różnicy wartość zamówień z dokładnie dwiema płatnościami, policzoną osobnym zapytaniem:

/* Niezależna kontrola: rzeczywisty stary raport minus nowy raport,
   porównane z wartością zamówień z dwiema płatnościami SUCCESS */
WITH Stary AS ( /* stary raport bez ORDER BY */ ),
Nowy AS ( /* nowy raport bez ORDER BY */ ),
DwiePlatnosci AS (
    SELECT DATEFROMPARTS(YEAR(o.OrderDate), MONTH(o.OrderDate), 1) AS SalesMonth,
           SUM(oi.Quantity * oi.UnitPrice * (100 - oi.DiscountPct) / 100) AS DoublePaidValue
    FROM dbo.Orders AS o
    JOIN dbo.OrderItems AS oi ON oi.OrderID = o.OrderID
    WHERE o.OrderDate >= '20250101' AND o.OrderDate < '20260101'
      AND (SELECT COUNT(*) FROM dbo.Payments AS p
           WHERE p.OrderID = o.OrderID AND p.Status = 'SUCCESS') = 2
    GROUP BY DATEFROMPARTS(YEAR(o.OrderDate), MONTH(o.OrderDate), 1)
)
SELECT s.SalesMonth,
       s.PaidRevenue AS OldReport,
       n.PaidRevenue AS NewReport,
       s.PaidRevenue - n.PaidRevenue AS Difference,
       d.DoublePaidValue,
       s.PaidRevenue - n.PaidRevenue - d.DoublePaidValue AS Unexplained
FROM Stary AS s
JOIN Nowy AS n ON n.SalesMonth = s.SalesMonth
JOIN DwiePlatnosci AS d ON d.SalesMonth = s.SalesMonth
ORDER BY s.SalesMonth;
/* Niezależna kontrola: rzeczywisty stary raport minus nowy raport,
   porównane z wartością zamówień z dwiema płatnościami SUCCESS */
WITH Stary AS (
SELECT DATEFROMPARTS(YEAR(o.OrderDate), MONTH(o.OrderDate), 1) AS SalesMonth,
       SUM(oi.Quantity * oi.UnitPrice * (100 - oi.DiscountPct) / 100) AS PaidRevenue
FROM dbo.Orders AS o
JOIN dbo.Payments AS p ON p.OrderID = o.OrderID
                      AND p.Status = 'SUCCESS'
JOIN dbo.OrderItems AS oi ON oi.OrderID = o.OrderID
WHERE o.OrderDate >= '20250101' AND o.OrderDate < '20260101'
GROUP BY DATEFROMPARTS(YEAR(o.OrderDate), MONTH(o.OrderDate), 1)
),
Nowy AS (
SELECT
    DATEFROMPARTS(YEAR(o.OrderDate), MONTH(o.OrderDate), 1) AS SalesMonth,
    SUM(
        oi.Quantity * oi.UnitPrice * (100 - oi.DiscountPct) / 100
    ) AS PaidRevenue
FROM dbo.Orders AS o
JOIN dbo.OrderItems AS oi
    ON oi.OrderID = o.OrderID
WHERE o.OrderDate >= '20250101'
  AND o.OrderDate <  '20260101'
  AND EXISTS (
      SELECT 1
      FROM dbo.Payments AS p
      WHERE p.OrderID = o.OrderID
        AND p.Status = 'SUCCESS'
  )
GROUP BY
    DATEFROMPARTS(YEAR(o.OrderDate), MONTH(o.OrderDate), 1)
),
DwiePlatnosci AS (
    SELECT DATEFROMPARTS(YEAR(o.OrderDate), MONTH(o.OrderDate), 1) AS SalesMonth,
           SUM(oi.Quantity * oi.UnitPrice * (100 - oi.DiscountPct) / 100) AS DoublePaidValue
    FROM dbo.Orders AS o
    JOIN dbo.OrderItems AS oi ON oi.OrderID = o.OrderID
    WHERE o.OrderDate >= '20250101' AND o.OrderDate < '20260101'
      AND (SELECT COUNT(*) FROM dbo.Payments AS p
           WHERE p.OrderID = o.OrderID AND p.Status = 'SUCCESS') = 2
    GROUP BY DATEFROMPARTS(YEAR(o.OrderDate), MONTH(o.OrderDate), 1)
)
SELECT s.SalesMonth,
       s.PaidRevenue AS OldReport,
       n.PaidRevenue AS NewReport,
       s.PaidRevenue - n.PaidRevenue AS Difference,
       d.DoublePaidValue,
       s.PaidRevenue - n.PaidRevenue - d.DoublePaidValue AS Unexplained
FROM Stary AS s
JOIN Nowy AS n ON n.SalesMonth = s.SalesMonth
JOIN DwiePlatnosci AS d ON d.SalesMonth = s.SalesMonth
ORDER BY s.SalesMonth;
Niezależna kontrola w SSMS: różnica między rzeczywistym starym i nowym raportem w każdym miesiącu równa się wartości zamówień z dwiema płatnościami, kolumna Unexplained wynosi zero
Krok 7. Kontrola niezależna: w każdym miesiącu różnica między rzeczywistymi wynikami obu raportów równa się wartości zamówień z dwiema płatnościami. Łącznie 355 257 263,92 zł, kolumna Unexplained wszędzie 0. To jest dowód.

Co AI zrobiło w tym studium dobrze: trafnie uszeregowało hipotezy, do każdej dało działające zapytanie diagnostyczne, a poprawka (EXISTS zamiast złączenia) jest prosta i poprawna. Na naszą prośbę nie skorygowało zapytania od razu, więc nie „naprawiło” zwrotów ani statusów, które okazały się fałszywym tropem. Czego AI nie zrobiło: nie znało reguły księgowej (musieliśmy ją podać) i zaproponowało kontrolę, która niczego nie sprawdza. Ten drugi punkt warto zapamiętać na zawsze: zapytanie weryfikujące musi liczyć stronę porównawczą inną drogą niż sprawdzane zapytanie. Jeśli obie strony wynikają z tego samego wzoru, zgodność jest gwarantowana i nic nie dowodzi.

20 błędów SQL, które AI potrafi znaleźć

Każdy z dwudziestu błędów poniżej ma tę samą anatomię: zapytanie wykonuje się bez żadnego komunikatu i zwraca liczbę, która wygląda wiarygodnie, ale jest zła. Do każdego przygotowaliśmy parę zapytań, wersję z błędem i wersję poprawną, uruchomiliśmy obie w SSMS w jednym oknie (zrzut pokazuje oba wyniki jeden pod drugim) i zapisaliśmy prawdziwe liczby z DemoShop. Wszystkie 20 par są do pobrania w pliku bledy_logiczne_20.sql.

Potem w jednej rozmowie daliśmy ChatGPT po kolei cel biznesowy i wersję z błędem, bez podpowiedzi, czego szukać. Pierwsza wiadomość zawierała opis schematu i zasadę odpowiadania, kolejne miały stały układ:

Cel zapytania: [cel biznesowy jednym zdaniem]

[zapytanie]

Czy to zapytanie realizuje ten cel? Jeśli nie, wskaż błąd i podaj poprawioną wersję.

Wynik: w 19 przypadkach na 20 ChatGPT wskazał dokładnie ten błąd, który zaszyliśmy. Każdą z tych 19 poprawek uruchomiliśmy i każda dała ten sam wynik co nasza wersja poprawna (w przypadku nr 2 zamiast liczby listę 7 410 klientów, czyli dokładnie to, o co prosił cel). W jednym przypadku (błąd 11) zrozumiał cel inaczej, niż zakładaliśmy, i odpowiedział na inne pytanie. Ten przypadek jest najciekawszy z całej listy.

NrBłądWynik z błędemWynik poprawnyAI
1JOIN zwielokrotnia wiersze757 450150 487wykrył
2Warunek z LEFT JOIN w WHERE5 5507 410wykrył
3COUNT(*) po LEFT JOIN01 860wykrył
4NULL w porównaniu39 871749 654wykrył
5NOT IN z NULL w podzapytaniu024wykrył
6SUM(DISTINCT)196 064,365 108 720,66wykrył
7UNION zamiast UNION ALL102 645 885,53105 772 529,64wykrył
8Granica zakresu dat754 197757 450wykrył
9BETWEEN na przedziałach2 062 030 wierszy2 052 953 wierszewykrył
10Strefa czasowa6 596 770,286 752 338,49wykrył
11Zduplikowane konta klientów150 487150 171inna interpretacja
12Średnia ze średnich4 956,843 155,97wykrył
13Zły poziom szczegółowości3 380 318,112 607 034,48wykrył
14Dzielenie całkowite04,49%wykrył
15LAST_VALUE bez ramkidata bieżącego zamówieniadata ostatniego zamówieniawykrył
16Zły PARTITION BY100% dla każdego9,61%, 5,35%...wykrył
17Domyślna ramka RANGE13 861 015,91 od pierwszej wpłaty714,76, 5 021,60...wykrył
18Iloczyn kartezjański8 402 94037wykrył
19AND i OR bez nawiasów2 220 814723 421wykrył
20ISNULL obcina tekst„br”„brak danych”wykrył

1. JOIN, który mnoży wiersze

Cel: ilu klientów złożyło zamówienie w 2025 roku. Wersja z błędem łączy klientów z zamówieniami i liczy COUNT(c.CustomerID), więc klient z dziesięcioma zamówieniami liczy się dziesięć razy. Wynik 757 450 to w rzeczywistości liczba zamówień. Poprawnie: COUNT(DISTINCT c.CustomerID), czyli 150 487. ChatGPT dodał jeszcze uproszczenie: złączenie z Customers nie jest tu w ogóle potrzebne, bo CustomerID jest w tabeli zamówień.

Dwa zapytania w SSMS: COUNT po złączeniu klientów z zamówieniami zwraca 757 450, a COUNT DISTINCT zwraca 150 487 klientów
Jeden zrzut, dwie wersje: z błędem 757 450, poprawna 150 487. Pięciokrotna różnica bez żadnego ostrzeżenia ze strony serwera.

2. Warunek na tabeli z LEFT JOIN w klauzuli WHERE

Cel: wszyscy klienci z Gdańska z liczbą zamówień w 2025 roku, także ci bez zamówień. Warunek na dacie zamówienia w WHERE odrzuca wiersze, w których po złączeniu zewnętrznym kolumny zamówienia są puste, więc LEFT JOIN zachowuje się jak INNER JOIN. Znika 1 860 klientów: 5 550 zamiast 7 410. Warunek na tabeli dołączanej przez LEFT JOIN należy do ON.

Porównanie w SSMS: warunek na dacie zamówienia w WHERE daje 5550 klientów z Gdańska, a ten sam warunek w ON daje 7410
Warunek w WHERE: 5550 klientów. Ten sam warunek w ON: 7410. Różnica to klienci bez zamówień w 2025 roku, o których raport miał mówić.

3. COUNT(*) po LEFT JOIN

Cel: ilu klientów z Gdańska nie złożyło w 2025 roku żadnego zamówienia. Wersja z błędem grupuje po kliencie i szuka grup z COUNT(*) = 0. Takich grup nie ma nigdy, bo LEFT JOIN zostawia dla klienta bez zamówień jeden wiersz z pustymi kolumnami zamówienia, a COUNT(*) liczy wiersze, nie zamówienia. Wynik 0 zamiast 1 860. Poprawnie: COUNT(o.OrderID), które pomija NULL.

Porównanie w SSMS: HAVING COUNT(*) = 0 zwraca zero klientów bez zamówień, a HAVING COUNT(o.OrderID) = 0 zwraca 1860
COUNT(*) liczy także wiersz dołożony przez LEFT JOIN, więc wynik wynosi 0. COUNT(o.OrderID) daje prawdziwe 1860.

4. NULL w porównaniu

Cel: ile zamówień z 2025 roku złożono bez kodu BLACK20. Warunek PromoCode <> 'BLACK20' dla zamówień bez żadnego kodu (NULL) nie jest ani prawdziwy, ani fałszywy, tylko nieznany, więc wiersz odpada. Zostaje 39 871 zamówień z innymi kodami, a powinno być 749 654. W SQL Server 2022 najkrócej poprawić to przez PromoCode IS DISTINCT FROM 'BLACK20', we wcześniejszych wersjach przez (PromoCode <> 'BLACK20' OR PromoCode IS NULL).

Porównanie w SSMS: warunek PromoCode różne od BLACK20 pomija zamówienia bez kodu i zwraca 39 871, a IS DISTINCT FROM zwraca 749 654
39 871 zamiast 749 654. Ponad 700 tysięcy zamówień bez kodu promocyjnego zniknęło, bo porównanie z NULL nie daje prawdy.

5. NOT IN, gdy podzapytanie zwraca NULL

Cel: którzy pracownicy nie są opiekunami żadnego zamówienia. Orders.EmployeeID jest pusty dla zamówień klientów indywidualnych. Wystarczy jeden NULL w wyniku podzapytania, żeby NOT IN nie zwróciło niczego: 0 zamiast 24. NOT EXISTS nie ma tego problemu.

Porównanie w SSMS: NOT IN z podzapytaniem zawierającym NULL zwraca 0 pracowników, NOT EXISTS zwraca 24
NOT IN: 0 pracowników. NOT EXISTS: 24. Jeden NULL w podzapytaniu wystarczy, żeby wynik był pusty.
ChatGPT wskazuje, że Orders.EmployeeID może być NULL, więc NOT IN może nie zwrócić żadnego pracownika, i poprawia zapytanie na NOT EXISTS
ChatGPT od razu łączy objaw z kolumną, która dopuszcza NULL (zna to z opisu schematu), i proponuje NOT EXISTS. Przy okazji zauważa, że cel brzmi „którzy pracownicy”, a zapytanie zwraca tylko ich liczbę.

6. SUM(DISTINCT)

Cel: przychód z produktu o identyfikatorze 1 w 2025 roku. SUM(DISTINCT ...) sumuje tylko różne wartości pozycji, więc wszystkie pozycje o tej samej wartości (ta sama cena, ilość i rabat) liczą się raz. Wynik 196 064,36 zł zamiast 5 108 720,66 zł. DISTINCT w funkcji agregującej prawie zawsze jest błędem, chyba że naprawdę chcesz policzyć różne wartości.

Porównanie w SSMS: SUM DISTINCT wartości pozycji daje 196 064,36 zł przychodu z produktu, a SUM daje 5 108 720,66 zł
SUM(DISTINCT): 196 064,36 zł. SUM: 5 108 720,66 zł. Ponad 26 razy mniej, bo powtarzające się wartości pozycji policzyły się po razie.

7. UNION zamiast UNION ALL

Cel: suma udanych wpłat BLIK i kartą w grudniu 2025 roku. UNION usuwa powtarzające się wiersze, a wiersz to tu tylko dzień i kwota. Dwie różne wpłaty na tę samą kwotę tego samego dnia zlewają się w jedną. Brakuje 3 126 644,11 zł. UNION ALL zachowuje wszystkie wiersze i jest przy okazji szybszy, bo nie musi sortować ani porównywać.

Porównanie w SSMS: UNION gubi wpłaty o tej samej kwocie i dniu, daje 102 645 885,53 zł, a UNION ALL daje 105 772 529,64 zł
UNION: 102 645 885,53 zł. UNION ALL: 105 772 529,64 zł. Różnica to wpłaty, które przypadkiem miały tę samą kwotę tego samego dnia.

8. Granica zakresu dat

Cel: ile zamówień złożono w 2025 roku. OrderDate przechowuje datę z godziną, więc <= '2025-12-31' oznacza „do północy 31 grudnia” i gubi cały ostatni dzień roku: 754 197 zamiast 757 450. Bezpieczny wzorzec dla każdego typu daty to przedział półotwarty: >= '2025-01-01' AND < '2026-01-01'.

Porównanie w SSMS: warunek do 31 grudnia włącznie gubi zamówienia z ostatniego dnia roku, 754 197 zamiast 757 450
3253 zamówienia z 31 grudnia zniknęły, bo granica zakresu wskazywała północ, a nie koniec dnia.

9. BETWEEN na przedziałach z historii cen

Cel: cena obowiązująca w dniu zamówienia. Historia cen zapisuje okresy jako [ValidFrom, ValidTo), czyli ValidTo to pierwszy dzień nowej ceny. BETWEEN obejmuje oba końce, więc pozycja z dnia zmiany ceny pasuje do starej i nowej ceny naraz. Złączenie zwraca 2 062 030 wierszy przy 2 052 953 pozycjach: 9 077 pozycji się podwoiło i każda suma oparta na tym złączeniu jest zawyżona.

Porównanie w SSMS: BETWEEN dopasowuje pozycje z dnia zmiany ceny do dwóch cen, 2 062 030 wierszy przy 2 052 953 pozycjach
Kontrolny trik: obok COUNT(*) policz COUNT(DISTINCT klucza). Gdy liczby się różnią, złączenie mnoży wiersze.
ChatGPT wyjaśnia, że okres obowiązywania ceny jest półotwarty, a BETWEEN obustronnie domknięty, i podaje warunek z ValidFrom i ValidTo
ChatGPT czyta definicję okresu z opisu tabeli w schemacie i dobiera właściwy warunek. Bez tego opisu nie miałby skąd wiedzieć, że ValidTo to pierwszy dzień nowej ceny.

10. Strefa czasowa

Cel: suma udanych wpłat z 1 stycznia 2026 roku według czasu polskiego. PaidAtUtc jest w UTC, a 1 stycznia w Polsce trwa od 23:00 UTC 31 grudnia do 23:00 UTC 1 stycznia. Obcięcie do daty w UTC przesuwa granicę o godzinę: 6 596 770,28 zł zamiast 6 752 338,49 zł.

Porównanie w SSMS: suma wpłat z 1 stycznia liczona w UTC daje 6 596 770,28 zł, a po przeliczeniu na czas polski 6 752 338,49 zł
Doba w UTC i doba w Polsce to dwa różne zakresy przesunięte o godzinę. Różnica 155 568,21 zł to saldo dwóch godzin: ostatniej godziny 31 grudnia w UTC, która w Polsce jest już 1 stycznia, i ostatniej godziny 1 stycznia w UTC, która w Polsce jest już 2 stycznia.
ChatGPT poprawia zapytanie, zamieniając granice polskiej doby na UTC operatorem AT TIME ZONE, tak aby warunek dotyczył samej kolumny PaidAtUtc
Poprawka ChatGPT jest lepsza od naszej: zamiast przeliczać każdą płatność na czas polski, przelicza na UTC tylko dwie granice doby. Wynik identyczny: 6 752 338,49 zł.

Różnicę między obiema poprawkami zmierzyliśmy na tymczasowym indeksie na PaidAtUtc. Wersja od AI zostawia kolumnę bez funkcji, więc serwer wyszukuje w indeksie tylko zakres jednej doby: 12 odczytów stron i 1 ms. Nasza przelicza strefę czasową dla każdej z 2,6 mln płatności: 11 686 odczytów, 6,7 s czasu i 25,7 s czasu procesora. Operator AT TIME ZONE jest wygodny, ale kosztowny, i lepiej używać go na stałych niż na kolumnie.

11. Zduplikowane konta: tu AI odpowiedziało na inne pytanie

Cel: ile różnych osób kupiło coś w 2025 roku. Wśród kupujących w DemoShop 316 osób ma po dwa konta klienta z tym samym adresem e-mail, w 142 parach różniącym się tylko wielkością liter. COUNT(DISTINCT CustomerID) liczy konta: 150 487. Osób, liczonych po adresie e-mail, jest 150 171.

Porównanie w SSMS: liczba różnych CustomerID 150 487 i liczba różnych adresów e-mail 150 171 wśród kupujących w 2025 roku
Kont jest o 316 więcej niż osób. Ze schematu tego nie widać, to cecha danych.
ChatGPT interpretuje różne osoby jako klientów indywidualnych B2C i dodaje filtr segmentu, zamiast wykryć zduplikowane konta
ChatGPT zrozumiał „osoby” jako „klienci indywidualni, nie firmy” i dodał filtr Segment = 'B2C'. To rozsądna interpretacja, ale inna niż nasza. O zduplikowanych kontach nie mógł wiedzieć, bo nie widział danych.

Wszystkie pozostałe błędy wynikały z kodu i dało się je znaleźć, czytając zapytanie i schemat. Ten wynika z danych: z tego, że ktoś założył drugie konto. AI, które widzi tylko kod i opis tabel, takiego problemu nie znajdzie. Znajdzie go zapytanie kontrolne, na przykład porównanie COUNT(DISTINCT CustomerID) z COUNT(DISTINCT LOWER(Email)), i warto o takie zapytania prosić AI wprost: „jakimi zapytaniami sprawdzę, czy w danych nie ma duplikatów, które zawyżą ten wynik?”.

12. Średnia ze średnich

Cel: średnia wartość zamówienia w 2025 roku dla całego sklepu. Wersja z błędem liczy średnią osobno w segmentach B2B i B2C, a potem uśrednia te dwie liczby. Obie grupy dostają tę samą wagę, choć zamówień B2C jest 3,5 raza więcej (564 530 wobec 158 891). Wynik 4 956,84 zł zamiast 3 155,97 zł.

Porównanie w SSMS: średnia ze średnich segmentów daje 4 956,84 zł, a średnia po wszystkich zamówieniach 3 155,97 zł
Średnia ze średnich zawyża wynik o 57%, bo zamówienia firmowe, średnio prawie pięć razy większe (8 168,55 zł wobec 1 745,14 zł), ważą w niej tyle samo co 3,5 raza liczniejsze zamówienia indywidualne.

13. Zły poziom szczegółowości

Cel: łączny koszt dostawy zamówień z 2025 roku. Koszt dostawy jest zapisany raz na zamówienie, a złączenie z pozycjami powtarza go tyle razy, ile zamówienie ma pozycji. Wynik 3 380 318,11 zł zamiast 2 607 034,48 zł. Przed każdą sumą zadaj sobie pytanie, na jakim poziomie (zamówienie czy pozycja) zapisana jest sumowana kolumna.

Porównanie w SSMS: koszt dostawy zsumowany po złączeniu z pozycjami daje 3 380 318,11 zł, bez złączenia 2 607 034,48 zł
Koszt dostawy powtórzony przy każdej pozycji zamówienia zawyża sumę o 773 283,63 zł.

14. Dzielenie całkowite

Cel: jaki procent zamówień z 2025 roku anulowano. W SQL Server dzielenie liczby całkowitej przez liczbę całkowitą daje liczbę całkowitą, więc 34 029 / 757 450 to 0, a 0 razy 100 to nadal 0. Wystarczy jedna liczba dziesiętna na początku wyrażenia: 100.0 * SUM(...) / COUNT(*) daje 4,49%.

Porównanie w SSMS: odsetek anulowanych zamówień liczony dzieleniem całkowitym daje 0, a z liczbą 100.0 na początku daje 4,49
Ten sam wzór, inna kolejność: 0 albo 4,49%. Błąd szczególnie zdradliwy w raportach, gdzie zero wygląda jak „nic się nie stało”.
ChatGPT wyjaśnia dzielenie całkowite w SQL Server i przenosi 100.0 na początek wyrażenia
ChatGPT wyjaśnia przyczynę jednym zdaniem i pokazuje, dlaczego mnożenie przez 100 na końcu nie pomaga: wynik dzielenia jest już zerem.

15. LAST_VALUE bez ramki okna

Cel: data ostatniego zamówienia klienta przy każdym jego zamówieniu. Z ORDER BY w klauzuli OVER domyślna ramka kończy się na bieżącym wierszu, więc „ostatnia” wartość to po prostu data bieżącego zamówienia. Trzeba jawnie rozszerzyć ramkę do końca partycji, czyli grupy wierszy wyznaczonej przez PARTITION BY (tu: wszystkich zamówień jednego klienta): ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING.

Porównanie w SSMS: LAST_VALUE z domyślną ramką zwraca datę bieżącego zamówienia, a z ramką do końca partycji datę ostatniego zamówienia 2026-09-28
Bez ramki LastOrderDate powtarza OrderDate. Z ramką do końca partycji każdy wiersz ma 2026-09-28, data ostatniego zamówienia klienta.

16. Zły PARTITION BY

Cel: udział każdego produktu w przychodzie jego kategorii. Suma w mianowniku jest liczona w partycji produktu, więc każdy produkt dzieli się sam przez siebie i ma 100%. Partycja musi odpowiadać całości, do której porównujemy, czyli kategorii.

Porównanie w SSMS: udział produktu liczony z PARTITION BY ProductID daje 100% dla każdego, a z PARTITION BY CategoryID 9,61% i 5,35%
100% przy każdym produkcie to sygnał, który łatwo przeoczyć w długim raporcie. Poprawnie: 9,61%, 5,35% i dalej.

17. Domyślna ramka RANGE przy powtarzających się wartościach

Cel: narastająca suma wpłat z 1 grudnia, wpłata po wpłacie. Okno jest posortowane po dacie (bez godziny), a wszystkie wpłaty mają tę samą datę. Domyślna ramka RANGE traktuje równe wartości jako jedną grupę, więc każdy wiersz dostaje sumę całego dnia: 13 861 015,91 zł już przy pierwszej wpłacie. Pomaga jednoznaczny porządek i ramka ROWS.

Porównanie w SSMS: suma narastająca z domyślną ramką RANGE pokazuje sumę całego dnia przy każdej wpłacie, z ramką ROWS narasta wpłata po wpłacie
RANGE: 13 861 015,91 zł w każdym wierszu. ROWS z jednoznacznym porządkiem: 714,76 zł, 5 021,60 zł i dalej, jak powinno.
ChatGPT wyjaśnia, że sortowanie okna po samej dacie nie ustala kolejności wpłat w ciągu dnia, i dodaje porządek po PaidAtUtc i PaymentID z ramką ROWS
ChatGPT poprawia dwie rzeczy naraz: porządek (pełny czas płatności plus PaymentID jako rozstrzygnięcie remisów) i ramkę ROWS.

18. Przypadkowy iloczyn kartezjański

Cel: zamówienia z Wigilii 2025 roku od klientów z Gdańska. Tabele są wymienione po przecinku w FROM, ale brakuje warunku, który je łączy. Każde z zamówień z Wigilii zostaje połączone z każdym klientem z Gdańska: 8 402 940 wierszy zamiast 37. Pisz złączenia jawnie przez JOIN ... ON, wtedy brak warunku jest błędem składni, a nie cichym mnożeniem.

Porównanie w SSMS: złączenie bez warunku zwraca 8 402 940 wierszy, a złączenie po CustomerID 37 zamówień
Brak jednego warunku: 8 402 940 wierszy zamiast 37. Na większych tabelach takie zapytanie potrafi zająć serwer na długie minuty.

19. AND i OR bez nawiasów

Cel: zamówienia doręczone albo zwrócone, złożone w 2025 roku. AND wiąże silniej niż OR, więc warunek oznacza „wszystkie doręczone z dowolnego roku albo zwrócone z 2025”. Wynik jest ponad trzy razy za duży: 2 220 814 zamiast 723 421. To ta sama pułapka co przy błędzie składniowym nr 2, tylko bez komunikatu.

Porównanie w SSMS: warunek z OR i AND bez nawiasów zwraca 2 220 814 zamówień, a z IN tylko 723 421
Bez nawiasów: 2 220 814 zamówień. Z IN: 723 421. Kolejność działań logicznych zmienia wynik ponad trzykrotnie.

20. ISNULL obcina tekst do typu kolumny

Cel: lista krajów klientów, a dla brakującego kraju napis „brak danych”. ISNULL zwraca typ pierwszego argumentu, a Country to char(2). Napis zastępczy zostaje obcięty do „br”. COALESCE wybiera typ według pierwszeństwa typów i zwraca pełny tekst.

Porównanie w SSMS: ISNULL z kolumną char(2) zwraca br zamiast brak danych, COALESCE zwraca pełny napis
ISNULL: „br”. COALESCE: „brak danych”. Liczba klientów ta sama, tylko etykieta obcięta, więc w raporcie łatwo to przeoczyć.
ChatGPT wskazuje, że ISNULL zwraca typ char(2) kolumny Country i obcina tekst zastępczy, oraz zauważa, że TOP (3) nie pasuje do celu
ChatGPT znalazł dwa problemy, a nie jeden: obcięcie przez typ char(2) i TOP (3), które nie pasuje do celu „lista krajów”. Drugiego nie zaplanowaliśmy, ale ma rację.

Wniosek z tej listy jest dwojaki. Po pierwsze, AI z opisem schematu wyłapuje błędy logiczne w kodzie bardzo skutecznie, często szybciej niż człowiek, i potrafi dorzucić uwagę, o którą nie prosiliśmy (błędy 5 i 20). Po drugie, nie zastąpi zapytań kontrolnych na danych: błąd 11 wymagał spojrzenia w dane, a nie w kod. Najlepsze efekty daje połączenie obu podejść, takie jak w studium przypadku z poprzedniej sekcji.

✕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

Jak znaleźć błąd w zapytaniu SQL za pomocą AI?
Przy błędzie składni wklej zapytanie razem z pełnym komunikatem serwera i wersją bazy. Przy błędzie logicznym podaj cel zapytania, objaw i liczbę odniesienia, na przykład sumę z księgowości, i poproś o hipotezy z zapytaniami diagnostycznymi.
Dlaczego zapytanie SQL działa bez błędu, a wynik jest zły?
Najczęściej przez złączenie, które mnoży wiersze, warunek na tabeli z LEFT JOIN umieszczony w WHERE, porównanie z NULL, granice dat albo dzielenie całkowite. Serwer nie zgłosi takiego błędu, bo zapytanie jest poprawne składniowo.
Czy AI zawsze znajdzie błąd logiczny w SQL?
Nie. AI widzi kod, a nie dane, więc błędy wynikające z danych, takie jak zduplikowane konta klientów, wymagają zapytań kontrolnych na bazie. W naszych próbach model raz odpowiedział na inne pytanie, niż zakładał autor zapytania.
Jak sprawdzić poprawkę błędu SQL zaproponowaną przez AI?
Porównaj wynik poprawionego zapytania z niezależnym źródłem, na przykład księgowością, i uruchom zapytanie kontrolne, które liczy różnicę inną drogą. Kontrola napisana przez AI według tej samej logiki co poprawka niczego nie dowodzi.

Komentarze (0)

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

Brak komentarzy...