In SQL gibt es verschiedene Datentypen, um Zahlen mit Nachkommastellen in einer Tabelle zu speichern. Der Datentyp float gehört nicht zu den genauen, sondern zu den ungefähren numerischen Datentypen. Das sollte bereits ein Hinweis dafür sein, warum dieser Datentyp nicht immer geeignet ist. Dieser Beitrag zeigt, wie es bei relationalen Eingabeanwendungen zu Rundungsfehlern im Zusammenhang mit float kommen kann und wie man diese vermeidet.
In unserem Beitrag „Float wie fließen“ haben wir bereits 2017 auf Probleme mit dem Datentyp float hingewiesen. Er beschreibt, wie Fehler beim Runden von beliebigen berechneten Zahlen mit der Funktion ROUND() und dem Datentyp float auftreten. Außerdem zeigt er, dass die Summierung von Zahlen dieses Datentyps nicht reproduzierbare Ergebnisse liefern kann. Der vorliegende Artikel beschreibt weitere Einschränkungen hinsichtlich der Verwendung von float für numerische Zahlenwerte.
Microsoft beschreibt den Datentyp float als „ungefähre Zahlendatentypen für numerische Gleitkommadaten. Gleitkommadaten sind Näherungswerte, deshalb können nicht alle Werte im Bereich des Datentyps exakt dargestellt werden.“
Dagegen sind die Datentypen decimal bzw. numeric für Zahlen mit fester Genauigkeit und mit fester Anzahl von Dezimalstellen vorgesehen. Für Währungswerte bieten sich die Datentypen money und smallmoney an, welche ebenfalls zu den Datentypen mit fester Genauigkeit gehören.
Das Problem mit float tritt vor allem dann auf, wenn Zahlen größer als eine Million verwendet werden. Ab dieser Schwelle setzt float intern und auch beim Konvertieren in einen String-Datentyp (char, varchar, nvarchar) den Wert in das wissenschaftliche Zahlenformat um. Ein Beispiel: Statt 1.500.000 wird 1.50000E+006 gespeichert.
Datentyp float vs. decimal bzw. money bei relationaler Dateneingabe
Die Konsequenz aus der wissenschaftlichen Schreibweise wird mit folgendem Beispiel deutlich. Dazu bauen wir uns eine Tabelle im SQL-Managementstudio in einer beliebigen Datenbank:
CREATE TABLE [dbo].[T_D_Budget_float_decimal](
[lfdnr] [int] IDENTITY(1,1) NOT NULL,
[JahrID] [int] NULL,
[Budgetwert_Sales_float] [float] NULL,
[Budgetwert_Units_float] [float] NULL,
[Budgetwert_Sales_money] [money] NULL,
[Budgetwert_Units_dec] [decimal](18, 2) NULL
)
Da wir diese Tabelle für eine relationale Eingabeanwendung verwenden wollen, setzen wir auf die IDENTITY-Spalte einen Primary Key:
ALTER TABLE [dbo].[T_D_Budget_float_decimal] ADD CONSTRAINT [PK_T_D_Budget_float_decimal] PRIMARY KEY CLUSTERED
(
[lfdnr] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
Wer möchte, baut an dieser Stelle zu der Tabelle eine View, welche alle Spalten der Tabelle enthält. Das Beispiel funktioniert aber auch nur mit der Tabelle.
Danach werden die Prozeduren für die relationale Eingabe erstellt (im Beispiel mit Verwendung einer View):
EXEC dbo.P_BC_Generate_DeltaMasterTableProc 'V_D_Budget_float_decimal', 'T_D_Budget_float_decimal'
Nun können wir mit DeltaMaster eine Eingabeanwendung bauen, indem wir unsere Anwendung mit der relationalen Datenbank verbinden, in welcher wir die Tabelle und die Prozeduren angelegt haben. Nachdem wir die Tabelle oder View in DeltaMaster hinzugefügt haben, bauen wir einen SQL-Durchgriffsbericht und speichern diesen ab. Nun müssen wir noch die Dateneingabe aktivieren und den Bericht für Dateneingaben zulassen.
Der Eingabebericht könnte dann wie folgt aussehen:
Jetzt können wir für ein beliebiges Jahr folgende Werte eingeben, sowohl für die float- wie auch für die money- bzw. decimal-Werte. In unserem Beispiel geben wir für Sales „10345678“ ein und für Units „10000123“. Nach Eingabe der Werte und noch vor dem Bestätigen sind alle Werte unverändert und korrekt:
Bestätigen wir die Werte per Klick auf den Haken über der Eingabe-Tabelle, werden diese von der Insert-Prozedur in die zuvor erstellte Datenbank-Tabelle übernommen.
Allerdings sind die float-Werte nun gerundet – die letzten beiden Stellen vor dem Komma entsprechen nicht mehr dem, was wir eingegeben haben.
Wo ist das Runden der Werte passiert? Schauen wir uns zunächst den Aufruf der Insert-Prozedur im SQL-Log an:
dbo.P_Insert_V_D_Budget_float_decimal
@lfdnr = NULL,
@JahrID = 2024,
@Budgetwert_Sales_float = 10345678,
@Budgetwert_Units_float = 10000123,
@Budgetwert_Sales_money = 10345678,
@Budgetwert_Units_dec = 10000123
Hier entsprechen alle Übergabeparameter der Eingabe. Also muss das Runden in der Prozedur oder durch den SQL-Optimierer auf Datenbankebene passieren. Dafür betrachten wir die Werte, welche über die Insert-Prozedur in die Tabelle T_D_Budget_float_decimal geschrieben werden:
SELECT
*
FROM T_D_Budget_float_decimal
Ergebnis:
Nun schauen wir uns die Insert-Prozedur P_Insert_V_D_Budget_float_decimal an:
ALTER PROCEDURE [dbo].[P_Insert_V_D_Budget_float_decimal]
(
@lfdnr nvarchar(255) = NULL ,
@JahrID nvarchar(255) = NULL ,
@Budgetwert_Sales_float nvarchar(255) = NULL ,
@Budgetwert_Units_float nvarchar(255) = NULL ,
@Budgetwert_Sales_money nvarchar(255) = NULL ,
@Budgetwert_Units_dec nvarchar(255) = NULL
)
Die Parameter der Prozedur sind alle auf nvarchar(255) gesetzt, um einen Fehler beim Aufruf der Prozedur durch fehlerhafte Eingaben zu verhindern. Innerhalb der Prozedur kann geprüft werden, ob die eingegebenen Daten dem korrekten Datentyp entsprechen, und kontrolliert eine Fehlermeldung ausgegeben werden.
Beispiel (siehe genannte Prozedur, Zeile 22):
if not isnumeric(isnull(@Budgetwert_Sales_float,0)) = 1 begin
raiserror('[Budgetwert_Sales_float] invalid!',16,1) return end
Durch die Übergabe eines float-Wertes in den Parameter mit dem Datentyp nvarchar() wird der Wert in das wissenschaftliche Datenformat konvertiert und aus 10345678 wird 10.3457E+006.
Beim Insert in die Tabelle T_D_Budget_float_decimal werden die Daten wieder in ihren jeweiligen Datentyp konvertiert:
INSERT INTO dbo.[T_D_Budget_float_decimal]
(
[JahrID],
[Budgetwert_Sales_float],
[Budgetwert_Units_float],
[Budgetwert_Sales_money],
[Budgetwert_Units_dec]
)
SELECT
cast(@JahrID as int ),
cast(@Budgetwert_Sales_float as float ),
cast(@Budgetwert_Units_float as float ),
cast(@Budgetwert_Sales_money as money ),
cast(@Budgetwert_Units_dec as decimal(18,2) )
Aus dem Wert 1.0346E+006 wird dabei 10345700.
Dass dies tatsächlich so passiert, können wir mit folgendem Beispiel belegen. Wir erstellen eine neue Tabelle:
CREATE TABLE [dbo].[T_Test_EingabeParameter](
[JahrID] [varchar](255) NULL,
[Budgetwert_Sales_float] [varchar](255) NULL,
[Budgetwert_Units_float] [varchar](255) NULL,
[Budgetwert_Sales_money] [varchar](255) NULL,
[Budgetwert_Units_dec] [decimal](18, 2) NULL
)
Das Befüllen der Tabelle fügen wir in die bestehende Prozedur P_Insert_V_D_Budget_float_decimal nach der Prüfung der numerischen Werte ein:
INSERT INTO dbo.T_Test_EingabeParameter (JahrID, Budgetwert_Sales_float, Budgetwert_Units_float, Budgetwert_Sales_money, Budgetwert_Units_dec)
SELECT @JahrID, @Budgetwert_Sales_float, @Budgetwert_Units_float, @Budgetwert_Sales_money, @Budgetwert_Units_dec
Da die Datentypen in dieser Tabelle alle nvarchar(255) sind und damit dem Datenformat der Parameter der Prozedur entsprechen, ist anzunehmen, dass in diese Tabelle exakt die Daten der Übergabeparameter geschrieben werden.
Dafür geben wir in unserer relationalen Eingabeanwendung ein neues Beispiel ein und speichern diese ab.
Sales: 10.345.678
Units: 10.123.456
Was wird in die Tabelle T_TEST_EingabeParameter geschrieben?
An dieser Stelle wird das Problem deutlich: Ab Werten größer als 1.000.000 setzt float intern den Wert in das wissenschaftliche Zahlenformat um. Das erklärt das Runden dieser Werte. Dass diese Werte nicht intern konvertiert und daher auch nicht gerundet werden, kann mit der gleichen relationalen Eingabeanwendung und der Eingabe von entsprechenden Werten getestet werden.
Fazit
Der Datentyp float ist mit Vorsicht einzusetzen. Für relationale Eingabeanwendungen ist er nicht empfehlenswert, da aufgrund der Konvertierung der Parameter der Eingabeprozeduren in nvarchar() und der damit verbundenen SQL-internen Umsetzung in das wissenschaftliche Datenformat bei float-Werten größer als 1 Million Rundungsfehler auftreten.