Kwota 19,99 zł w bazie po kilku operacjach zaczyna wyglądać jak 19,989999… i raport przestaje się zgadzać. Typ zwykle rozwiązuje problem: dla wartości wymagających dokładności wybierz DECIMAL(p, s), ale jego parametry i zachowanie zależą od silnika oraz wykonywanych działań.

DECIMAL przechowuje wartości dokładnie, jeśli dobierzesz właściwy zakres
- p oznacza łączną liczbę cyfr, a s liczbę cyfr po separatorze dziesiętnym.
- DECIMAL(10,2) pozwala zapisać do ośmiu cyfr przed separatorem i dwie po nim.
- Konwersja do mniejszej skali może zaokrąglić wartość, zamiast zgłosić błąd.
- Reguły obliczeń różnią się między silnikami, więc sprawdź typ wyniku dzielenia i mnożenia.
- FLOAT i DOUBLE są przybliżone; dla kwot zwykle lepiej sprawdza się typ dziesiętny.
DECIMAL(10,2) nie oznacza dziesięciu cyfr przed przecinkiem
Zapis DECIMAL(p, s) określa pojemność kolumny: p to maksymalna liczba cyfr łącznie, a s wskazuje, ile z nich może wystąpić po separatorze. W definicji DECIMAL(10,2) zostaje więc osiem cyfr na część całkowitą i dwie na ułamkową.
W praktyce taki typ pasuje do ceny lub kwoty o dwóch miejscach po przecinku, ale niekoniecznie do każdej liczby związanej z pieniędzmi. Jeśli przechowujesz kurs waluty, stawkę podatku albo wynik pomiaru, dwa miejsca po separatorze mogą uciąć potrzebną precyzję. Dobierz skalę do danych źródłowych i reguł biznesowych, nie do wyglądu przykładowego raportu.
Dokładność nie chroni przed cichym zaokrągleniem
Kolumna dziesiętna może przechowywać wartość dokładnie, a mimo to dostać inną liczbę niż ta podana w zapytaniu. Dzieje się tak, gdy wejście ma więcej cyfr ułamkowych, niż dopuszcza skala kolumny. Dokumentacja Microsoft Learn dla DECIMAL i NUMERIC w SQL Server opisuje zaokrąglanie przy konwersji do niższej precyzji lub skali.
Przykładowo kwota 12,345 zapisana w kolumnie o skali 2 może zostać zaokrąglona do dwóch miejsc. To nie to samo co błąd przepełnienia: za mało miejsca na część całkowitą może uniemożliwić zapis, podczas gdy nadmiar cyfr po separatorze może zostać zaokrąglony. Testuj oba przypadki, szczególnie przed migracją danych.
Ten sam zapis typu nie gwarantuje identycznego zachowania w każdej bazie
Przy przejściu między PostgreSQL, MySQL i SQL Server najłatwiej przeoczyć granice skali oraz limity deklaracji. PostgreSQL pozwala jawnie zadeklarować precyzję numeric do 1000 cyfr, podczas gdy SQL Server ogranicza precyzję decimal do 38, a MySQL dopuszcza maksymalnie 65 cyfr dla DECIMAL. To różnice istotne przy przenoszeniu schematu, nie ciekawostka dokumentacyjna.
Jest też mniej oczywisty szczegół: PostgreSQL dopuszcza ujemną skalę, na przykład NUMERIC(2,-3), która zaokrągla wartości do tysięcy. Ta składnia nie jest przenośna do każdego systemu SQL. Przed migracją sprawdź definicje kolumn oraz wynikowe typy wyrażeń, bo zgodna nazwa typu nie oznacza zgodnych reguł.
Dzielenie może obciąć precyzję, zanim wynik trafi do kolumny
Załóżmy, że liczysz jednostkową cenę jako sumę 100,00 podzieloną przez 3. Oczekiwanym wynikiem może być wartość z wieloma cyframi po przecinku, lecz konkretna baza ustala precyzję i skalę wyniku według reguł dla działania, a dopiero potem ewentualnie konwertuje go do typu kolumny.
W SQL Server wynik dzielenia typów dziesiętnych ma wyliczaną precyzję i skalę, a całkowita precyzja nie może przekroczyć 38. Gdy wynik wymaga więcej miejsca, skala może zostać ograniczona, co może doprowadzić do zaokrąglenia. Z kolei PostgreSQL zaznacza, że obliczenia na numeric są dokładne tam, gdzie to możliwe, ale mogą być wolniejsze od typów całkowitych i zmiennoprzecinkowych. Typ wejściowy wpływa więc na wynik pośredni, nie tylko na zapis końcowy.
Uważam, że przy kwotach bezpieczniej jawnie rzutować argumenty działania i świadomie zaokrąglić wynik dopiero w wybranym miejscu, zamiast liczyć na domyślne reguły silnika. Na przykład użyj CAST(... AS DECIMAL(12,4)) dla obliczenia ceny jednostkowej, a później osobno zdecyduj, czy wynik prezentować i zapisywać z dwiema cyframi.
| Sytuacja | Możliwa przyczyna | Co zrobić |
|---|---|---|
| Kwota po zapisie ma mniej cyfr ułamkowych | Skala kolumny jest mniejsza niż precyzja wejścia | Zweryfikuj wymaganą skalę i zachowanie konwersji |
| Wstawienie dużej wartości kończy się błędem | Brak miejsca na cyfry przed separatorem | Zwiększ precyzję albo ustal dozwolony zakres danych |
| Wynik dzielenia zaskakuje długością | Silnik wyznacza skalę wyrażenia według własnych reguł | Sprawdź typ wyniku i użyj jawnego rzutowania |
| Po migracji liczby zachowują się inaczej | Silniki mają różne limity i obsługę skali | Przetestuj wartości graniczne i działania arytmetyczne |
FLOAT nie zastępuje DECIMAL przy rozliczaniu kwot
FLOAT i DOUBLE zapisują wartości jako przybliżenia, dlatego nie każda liczba dziesiętna ma w nich dokładną reprezentację. Dokumentacja MySQL Reference Manual wskazuje DECIMAL jako typ dokładny, stosowany między innymi przy danych pieniężnych. W PostgreSQL real i double precision są opisane jako typy niedokładne, a numeric jako dokładny.
Reguła wyboru jest prosta. Jeśli wartość musi zachować ustaloną liczbę cyfr dziesiętnych, na przykład kwota faktury, stawka lub saldo, wybierz DECIMAL/NUMERIC; jeśli dane są pomiarami naukowymi, a dopuszczalne jest przybliżenie i ważna jest wydajność, rozważ typ zmiennoprzecinkowy. Pamiętaj też, że format wyświetlania to osobna sprawa: zera końcowe mogą być prezentowane inaczej niż oczekujesz, mimo że wartość jest poprawna.
Przed wdrożeniem sprawdź trzy przypadki graniczne
Najwięcej błędów wychodzi przy wartościach skrajnych, a nie przy zwykłym rekordzie z testowego środowiska. Przed wdrożeniem sprawdź największą dozwoloną kwotę, najmniejszy ułamek, który musi zostać zachowany, oraz rezultat dzielenia lub mnożenia używanego w aplikacji.
W testach uwzględnij także zapis z nadmiarem miejsc po separatorze i sprawdź, czy baza zaokrągla go zgodnie z oczekiwaniem. To krótki test, który często rozstrzyga, czy definicja DECIMAL(p,s) odpowiada rzeczywistemu zakresowi danych, a nie tylko przykładowej wartości w kodzie.
