Datenqualität in SQL Server // TRY_CONVERT für float und real sicher anwenden

Datenqualität bei Gleitkomma-Spalten ist eine eigene Disziplin — und TRY_CONVERT(float, …) hat ein paar Eigenheiten, die im Import-Pfad gerne übersehen werden: eine leere Zeichenfolge wird zu 0 (statt zu NULL), ein Komma als Dezimaltrennzeichen führt zu NULL, und selbst nach einer sauberen Konvertierung ergibt 2 + 3.4 - 3.4 - 2 als float nicht exakt 0. Dieser Artikel sortiert die Regeln, zeigt das sichere Konvertierungs-Pattern und macht den Unterschied zwischen decimal (präzise) und float (approximierend) explizit.

Kurzüberblick:

  • Zwei Gleitkomma-Typen mit klar abgestufter Präzision — real (= float(24), 4 Byte) und float (= float(53), 8 Byte).
  • Bei der Konvertierung aus Text akzeptiert TRY_CONVERT nur den Punkt als Dezimaltrennzeichen, ein Komma führt Locale-unabhängig zu NULL; wissenschaftliche Notation ('1.23E-3') wird unterstützt.
  • Eine leere Zeichenfolge ist der Sonderfall: sie liefert 0, was im ETL-Kontext fachlich meist falsch ist — ein CASE/TRIM/REPLACE-Pattern macht daraus sauber NULL.
  • float ist ein Approximations-Typ — Operationen erzeugen Rundungsfehler im Bereich 1E-16. Wer exakte Werte braucht (Geld, regulierte Reporting-Werte), greift zu decimal.
  • In Postgres wirft CAST(... AS double precision) eine Exception bei ungültigem Input — ein PL/pgSQL-Wrapper try_cast_double liefert das NULL-statt-Exception-Verhalten nach. IEEE 754 ist plattform-übergreifend identisch.

Voraussetzung: SQL Server 2017+ (TRY_CONVERT ist seit 2012 verfügbar; das sichere Pattern weiter unten verwendet TRIM, das ab SQL Server 2017 zur Verfügung steht — auf 2012–2016 stattdessen LTRIM(RTRIM(...))). Beispiele sind ohne Sample-Datensatz lauffähig — reine Inline-Literale und DECLARE-Variablen.

Inhalt

Überblick

SQL Server stellt für die Speicherung von Gleitkommazahlen zwei Datentypen zur Verfügung: float(n) und real. Beide sind keine präzisen Datentypen: sie speichern Werte in binärer Mantisse-/Exponent-Darstellung. Das hält große Wertebereiche mit moderatem Speicherbedarf abbildbar, geht aber zu Lasten der Genauigkeit. Eine float-Variable kann augenscheinlich den Wert 0 halten, in einer weniger signifikanten Nachkommastelle aber einen Rest ungleich 0 mitführen. Typische Anwendung: technische Messwerte, Sensor-Daten, naturwissenschaftliche Berechnungen — also überall dort, wo eine Genauigkeit von 7 bis 15 signifikanten Stellen ausreicht.

Die Microsoft-Dokumentation zu TRY_CONVERT (bzw. CAST und CONVERT) gibt an, dass der zu konvertierende Wert ein beliebiger Ausdruck sein kann. Damit kommen sowohl nvarchar-Texte aus CSV-/JSON-/XML-Imports als auch typisierte Zahlen (z. B. decimal(18, 5) aus einer Pipeline-Berechnung) als Input in Frage. Der Artikel unterscheidet zwischen den beiden Fällen — Text und Kein Text — und stellt im Anschluss ein sicheres Konvertierungs-Pattern vor.

Präzision und Speicherbedarf

Vor der Konvertierung steht die Typ-Wahl. Die zwei Gleitkomma-Typen unterscheiden sich durch die Anzahl der Bits, die für die Mantisse (Zahl vor dem Exponenten) reserviert werden — und damit durch Speicherbedarf und Genauigkeit:

DatentypMantisse-BitsBytesSignifikante DezimalstellenWertebereich (ca.)Typische Verwendung
real   (= float(24))244~7-3.4E+38..3.4E+38Sensor-Werte, Pixel-Koordinaten, einfache Messwerte
float  (= float(53))538~15-1.79E+308..1.79E+308Wissenschaftliche Berechnungen, hochpräzise Messwerte

Der Parameter n in float(n) darf Werte von 1 bis 53 annehmen. SQL Server fasst die Auflösung in zwei Buckets zusammen: für n von 1 bis 24 werden 4 Byte reserviert (Single Precision, identisch zu real); für n von 25 bis 53 werden 8 Byte reserviert (Double Precision, identisch zu float ohne Parameter). Fehlt die Angabe von n, nimmt SQL Server den Wert 53 an. Konkret: realfloat(24) und alles bis float(24) sind binärisch dasselbe; float(25) bis float(53) und float ohne Parameter sind binärisch dasselbe.

Dieser Artikel betrachtet überwiegend float ohne Parameter (also float(53)); die Unterschiede zu real betreffen nur Präzision und Speicher, nicht die Konvertierungs-Regeln.

Text

Wenn ein Text in einen float-Wert konvertiert werden soll, sind unter anderem die folgenden Szenarien zu untersuchen:

  • Der Text enthält eine gültige Zahl in dezimaler Notation ('123.456').
  • Der Text verwendet ein Komma als Dezimaltrennzeichen ('123,456').
  • Der Text ist eine leere Zeichenfolge oder enthält nur Leerzeichen.
  • Der Text verwendet wissenschaftliche Notation ('123456E-3').

Die folgenden Aufrufe von TRY_CONVERT decken diese Fälle ab. Hinter den Anweisungen ist das jeweils zurückgegebene Ergebnis notiert:

  1: SELECT TRY_CONVERT(float, NULL        ) -- NULL
  2: SELECT TRY_CONVERT(float, N'123'      ) -- 123
  3: SELECT TRY_CONVERT(float, N'123,456'  ) -- NULL
  4: SELECT TRY_CONVERT(float, N'123.456'  ) -- 123.456
  5: SELECT TRY_CONVERT(float, N''         ) -- 0
  6: SELECT TRY_CONVERT(float, N' '        ) -- 0
  7: SELECT TRY_CONVERT(float, N'  123.456') -- 123.456
  8: SELECT TRY_CONVERT(float, N'123.456  ') -- 123.456
  9: SELECT TRY_CONVERT(float, N'123456E-3') -- 123.456

Zahlen in dezimaler Notation mit Punkt als Trennzeichen werden wie erwartet konvertiert; führende und folgende Leerzeichen beeinflussen das Ergebnis nicht; wissenschaftliche Notation (Mantisse E Exponent) ebenfalls. TRY_CONVERT(float, …) ist Locale-unabhängig — weder SET LANGUAGE noch SET DATEFORMAT beeinflussen die Interpretation eines Dezimaltrennzeichens. Die Konvertierung erwartet konstant einen Punkt als Dezimaltrennzeichen, unabhängig von Server- oder Session-Einstellungen; ein Komma im Text führt deshalb zu NULL.

Damit hält das Ergebnis bereits zwei Überraschungen bereit. Zeile 3 zeigt: ein Komma im Text-Pfad ist nicht etwa als Tausendertrennzeichen oder als deutsches Dezimaltrennzeichen interpretierbar — der Aufruf scheitert. Wer aus einer CSV mit deutscher Notation importiert, muss vor TRY_CONVERT Komma zu Punkt normalisieren. Zeilen 5 und 6 zeigen: eine leere Zeichenfolge oder eine Zeichenfolge mit nur Leerzeichen wird zum Wert 0 konvertiert. Während das NULL-Ergebnis bei Komma nachvollziehbar ist, ist die 0-Konvertierung einer leeren Zeichenfolge im ETL-Kontext fachlich (meistens) falsch: ein leeres Feld einer CSV-Spalte ist semantisch ein „unbekannt“, kein „0″. Die sichere Variante ist weiter unten als Pattern beschrieben.

Kein Text

Wenn der Eingangswert nicht als Text vorliegt, sondern bereits typisiert ist (Ganzzahl oder Dezimalzahl), reduzieren sich die Szenarien auf:

  • Der übergebene Wert ist eine Ganzzahl oder eine typisierte Dezimalzahl.
  • Der übergebene Wert ist als Komma-Ausdruck angegeben (123, 456) — und wird vom Parser als zwei Argumente interpretiert.
  • Der übergebene Wert ist eine Zahl in wissenschaftlicher Notation.

Die ersten fünf Tests:

  1: SELECT TRY_CONVERT(float, NULL      ) -- NULL
  2: SELECT TRY_CONVERT(float, 123       ) -- 123
  3: SELECT TRY_CONVERT(float, 123, 456  ) -- 123
  4: SELECT TRY_CONVERT(float, 123.456   ) -- 123.456
  5: SELECT TRY_CONVERT(float, 123456E-3 ) -- 123.456

Zeile 3 verdient besondere Aufmerksamkeit. Der Aufruf sieht aus, als sollte er '123,456' als Dezimalzahl konvertieren — aber das Komma ist in T-SQL kein Dezimaltrennzeichen, sondern der Parameter-Separator zwischen Funktions-Argumenten. TRY_CONVERT ist als Drei-Argument-Funktion definiert ((data_type, expression, style)), und der Parser interpretiert die 456 als dritten Parameter (style). Für TRY_CONVERT(float, …) hat der style-Parameter keinen Effekt (Style-Codes sind nur für Datums- und Geld-Konvertierungen relevant), und übrig bleibt der erste Ausdruck 123. Im Klartext: TRY_CONVERT(float, 123, 456) liefert 123, nicht 123.456 und auch nicht NULL. Wer typisierte Zahlen mit Komma als Literal in Code schreibt, erzeugt unbemerkt einen falschen Wert — anders als im Text-Pfad, wo ein Komma sauber zu NULL führt.

Eine typisierte Dezimalzahl wird ansonsten unverändert in den float-Wertebereich übertragen (Zeile 4). Wissenschaftliche Notation funktioniert genauso wie im Text-Pfad (Zeile 5).

Decimal vs. Float

decimal ist im Unterschied zu float und real ein präziser Datentyp: er speichert Werte als signierte Folge von Dezimalziffern mit fester Stellenzahl (decimal(precision, scale)). Die Nachkommastellen werden exakt gehalten, eine Rechnung wie 2 + 3.4 - 3.4 - 2 ergibt verlässlich 0float speichert dagegen in Binärdarstellung mit fester Mantisse-Länge — viele Dezimalzahlen, die in der menschlichen Darstellung „glatt“ wirken (0.10.23.4), sind in dieser Binärdarstellung nicht exakt abbildbar. Folge: Operationen akkumulieren Rundungsfehler im Bereich der Mantisse-Auflösung.

Die gleiche Rechnung einmal float, einmal mit decimal:

  1: DECLARE @f1 AS float = 2;
  2: DECLARE @f2 AS float = 3.4;
  3: DECLARE @f3 AS float = @f1 + @f2;
  4: 
  5: SELECT @f3 - @f2 - @f1                  -- 4.44089209850063E-16
  6:                                         -- 0,0000000000000004440892...
  7: 
  8: DECLARE @d1 AS decimal(2, 1) = 2;
  9: DECLARE @d2 AS decimal(2, 1) = 3.4;
 10: DECLARE @d3 AS decimal(2, 1) = @d1 + @d2;
 11: 
 12: SELECT @d3 - @d2 - @d1                  -- 0.0

Im float-Pfad bleibt nach der Subtraktion ein Rest von 4.44E-16 — ein typischer Rundungsfehler innerhalb der Präzision von float’s 53-Bit-Mantisse (binary64-Format gemäß IEEE 754). Der konkrete Wert ist nicht „die Auflösungs-Grenze“, sondern eine Folge der akkumulierten Rundungsschritte in den drei beteiligten Operationen — derselbe Rechenweg auf demselben SQL Server liefert verlässlich genau diesen Rest. In real (24-Bit-Mantisse, binary32) wäre der Rest entsprechend größer, weil weniger Bits für die Nachkommagenauigkeit zur Verfügung stehen. Im decimal-Pfad ist das Ergebnis exakt 0.0, weil keine Binär-Approximation stattfindet.

Praktische Konsequenz: Für Werte mit fachlicher Exakt-Anforderung (Geld-Beträge, regulierte Reporting-Werte, Inventur-Mengen) ist decimal der Default. float und real sind für Werte mit vermessungstoleranter Bedeutung gedacht (Messwerte, Naturkonstanten, Berechnungen mit klar definierter Mess-Genauigkeit). Eine ausführliche Behandlung von decimal liefert TRY_CONVERT // Konvertierung nach decimal, numeric.

Caveat: decimal ist nicht ausnahmslos exakt — bei einer Konvertierung in eine niedrigere Skala rundet SQL Server still, ohne Fehler oder NULL. Beispielsweise liefert TRY_CONVERT(decimal(10, 0), 1234.5) den Wert 1235, nicht NULL und auch keinen Fehler (siehe FAQ).

Sichere Typ-Konvertierung

Die beiden Sektionen oben haben zwei Sonderfälle gezeigt, die im Import-Pfad explizit behandelt werden müssen:

  • Eine leere Zeichenfolge wird zu 0 konvertiert. Wenn das fachlich falsch ist (Default-Fall bei CSV-Imports), muss die leere Zeichenfolge vor dem TRY_CONVERT auf NULL gemappt werden.
  • Ein Komma als Dezimaltrennzeichen führt zu NULL. Wenn die Quelle deutsche Notation liefert ('123,456'), muss das Komma vor dem TRY_CONVERT durch einen Punkt ersetzt werden.

Das folgende Beispiel löst beide Sonderfälle gemeinsam:

  1: DECLARE @p_input AS nvarchar(30);
  2: SET @p_input = N'  123.45678';
  3: 
  4: SELECT TRY_CONVERT( float
  5:                   , REPLACE( CASE WHEN TRIM(@p_input) = ''
  6:                                 THEN NULL
  7:                                 ELSE @p_input
  8:                              END
  9:                             , ','
 10:                             , '.'
 11:                            )
 12:                   ) AS [Output];

Wer das Pattern in einem ETL-Prozess in mehreren Spalten gleichzeitig braucht, abstrahiert es zu einer benutzerdefinierten Funktion fn_try_convert_float(@p_input nvarchar) — siehe Design Pattern // Sichere Typ-Konvertierung mit T-SQL.

Postgres-Brücke

In Multi-Engine-ETL-Pipelines stellt sich die Frage nach dem Postgres-Pendant. Die gute Nachricht zuerst: IEEE 754 ist plattform-übergreifend identisch. Eine Zahl, die in SQL Server als float (binary64) gespeichert ist, hat in Postgres als double precision exakt dieselbe Bit-Repräsentation — und denselben Rundungsfehler. Cross-DB-Berechnungen liefern bei korrektem Cast bit-identische Ergebnisse.

Beim Konvertierungs-Verhalten gibt es allerdings zwei zentrale Unterschiede:

  • CAST(s AS double precision) ist Postgres‘ Default-Konvertierung — und wirft eine Exception bei ungültigem Input. Es gibt also kein direktes TRY_CONVERT-Pendant; der Aufruf läuft entweder durch oder bricht ab.
  • Auch Postgres 18 (Release 2025-09-25) hat kein eingebautes Pendant nachgezogen — weder eine try_cast-Funktion noch die SQL/JSON CAST ... ON ERROR NULL-Syntax aus SQL:2023 sind Teil von 18.0. Den NULL-statt-Exception-Wrapper schreibt man also weiterhin selbst als PL/pgSQL-Funktion.

Wrapper-Funktion als PL/pgSQL-Pendant zu TRY_CONVERT(float, …):

  1: CREATE OR REPLACE FUNCTION try_cast_double (p_input text)
  2:    RETURNS double precision
  3:    LANGUAGE plpgsql
  4:    IMMUTABLE
  5: AS $$
  6: BEGIN
  7:    IF p_input IS NULL OR TRIM(p_input) = '' THEN
  8:       RETURN NULL;
  9:    END IF;
 10:    RETURN p_input::double precision;
 11: EXCEPTION
 12:    WHEN invalid_text_representation OR numeric_value_out_of_range
 13:    THEN
 14:       RETURN NULL;
 15: END;
 16: $$;

Damit verhält sich try_cast_double('1,234') wie TRY_CONVERT(float, '1,234'): der Aufruf gibt NULL zurück, statt eine Exception zu werfen. Die IF-Klausel fängt zusätzlich den leeren-Zeichenfolge-Sonderfall ab — Postgres würde ::double precision auf einer leeren Zeichenfolge ebenfalls mit einer Exception quittieren, nicht (wie SQL Server) zu 0 konvertieren. Analog für real als try_cast_real(p_input text) RETURNS real.

Wenn die Eingabe formatierte Zahlen mit Tausender-/Dezimaltrennzeichen liefert (deutsche oder US-Notation), greift to_number mit einem Format-Pattern:

  1: SELECT to_number('1.234,56', 'FM999G999D99');  -- 1234.56  (deutsche Notation, lc_numeric = 'de_DE.UTF-8')
  2: SELECT to_number('1,234.56', 'FM999G999D99');  -- 1234.56  (US-Notation, lc_numeric = 'en_US.UTF-8')

to_number ist Locale-abhängig über die Session-Variable lc_numeric — anders als SQL-Servers TRY_CONVERT, das Locale-unabhängig ausschließlich den Punkt akzeptiert. Im ETL-Kontext lohnt es sich, lc_numeric pro Session explizit zu setzen, damit der Import von der Server-Default-Konfiguration entkoppelt ist. Achtung: to_number liefert numeric (präzise!) zurück; für double precision ist ein abschließender Cast notwendig (to_number(s, 'FM999G999D99')::double precision).

Typ-Mapping T-SQL ↔ Postgres:

SQL ServerPostgresIEEE-754-FormatBytes
float  (= float(53))double precisionbinary648
float(24) und realrealbinary324
TRY_CONVERT(float, s)try_cast_double(s) (Wrapper)
TRY_CONVERT(real, s)try_cast_real(s) (Wrapper)
TRY_CONVERT(float, s) mit Trennzeichento_number(s, 'FM999G999D99')::double precision

Zusammenfassung

Bei der Konvertierung eines Wertes in den Datentyp float gibt es drei Sonderfälle: eine leere Zeichenfolge wird zur Zahl 0, ein Komma als Dezimaltrennzeichen führt zu NULL (Locale-unabhängig), und im Code geschriebene Komma-Literale (123, 456) werden als Funktions-Parameter interpretiert. Das oben gezeigte sichere Konvertierungs-Pattern fängt die ersten beiden Fälle ab; der dritte ist eine Frage der Code-Disziplin (typisierte Zahlen im Source-Code immer mit Punkt schreiben).

Der Unterschied zwischen den präzisen (decimal) und approximierenden (floatreal) Datentypen spielt bei der reinen Konvertierung eines Wertes eine untergeordnete Rolle. Sobald aber mit float-Werten gerechnet wird — auch in Aggregationen innerhalb eines ETL-Prozesses — können sich Rundungsfehler akkumulieren. In solchen Fällen ist decimal dem float vorzuziehen.

Take-Away:

  • Typ-Wahl nach erwarteter Genauigkeit: real (binary32, ~7 signifikante Stellen) für Sensor-Werte und Pixel-Koordinaten, float (binary64, ~15 signifikante Stellen) für wissenschaftliche Berechnungen — decimal für alles mit fachlicher Exakt-Anforderung (Geld, Mengen, regulierte Reporting-Werte).
  • TRY_CONVERT(float, '') ergibt 0, nicht NULL — leere Zeichenfolgen vor der Konvertierung per CASE/TRIM auf NULL mappen.
  • TRY_CONVERT(float, '1,234') ergibt NULL (Locale-unabhängig, nur Punkt als Dezimaltrennzeichen) — Komma vor der Konvertierung per REPLACE(',', '.') normalisieren.
  • Postgres-Pendant: double precision (= float(53), binary64) und real (= float(24), binary32); kein eingebautes try_cast in Postgres 18.0, eigenen Wrapper schreiben. to_number liefert numeric und ist Locale-abhängig über lc_numeric.

FAQ

Warum gibt TRY_CONVERT(float, '1,234') NULL zurück?

TRY_CONVERT(float, ...) ist Locale-unabhängig und akzeptiert ausschließlich den Punkt als Dezimaltrennzeichen — weder SET LANGUAGE noch SET DATEFORMAT beeinflussen die Interpretation. Sobald ein Komma im String vorkommt, scheitert die Konvertierung. Wenn die Quelle deutsche Notation liefert ('1.234,56'), vor dem Aufruf per REPLACE säubern: REPLACE(REPLACE(@p_input, '.', ''), ',', '.') macht aus '1.234,56' ein '1234.56', das anschließend per TRY_CONVERT(float, …) konvertiert. Bei der Form ohne Tausendertrennzeichen reicht REPLACE(@p_input, ',', '.') (so wie im sicheren Pattern oben).

float oder real — welcher Typ wann?

Die Faustregel ist die Genauigkeitsanforderung der Quelle. real (= float(24), binary32, 4 Byte) reicht für Werte mit ~7 signifikanten Dezimalstellen — Sensor-Messungen, Pixel-Koordinaten, einfache geometrische Berechnungen. float (= float(53), binary64, 8 Byte) hält ~15 signifikante Stellen und ist der Default für wissenschaftliche Berechnungen, hochauflösende Messwerte und alles, wo Aggregationen über viele Werte Rundungsfehler akkumulieren würden. Speicher-Effizienz lohnt sich nur in Tabellen mit Milliarden Zeilen; in Standard-Schemen ist der Unterschied zwischen 4 und 8 Byte pro Zeile vernachlässigbar.

Warum ist 0.1 + 0.2 != 0.3 in SQL Server (und in jeder anderen IEEE-754-konformen Sprache)?

0.10.2 und 0.3 sind in binärer Mantisse-/Exponent-Darstellung nicht exakt abbildbar — analog zu 1/3 in dezimaler Darstellung, das nur als unendliche 0.333...-Folge geschrieben werden kann, lassen sich 0.1 und 0.2 in binary64 nur als periodische Bit-Folge darstellen und werden bei der Speicherung auf 53 Mantisse-Bits gerundet. 0.1 + 0.2 in float ergibt deshalb 0.30000000000000004 — der Rest stammt aus den Rundungs-Resten beider Summanden. decimal(2, 1) speichert die Werte dezimal-exakt und liefert die 0.3. Dasselbe Verhalten gilt in Postgres, Python, JavaScript und C — IEEE 754 ist ein plattform-übergreifender Standard.

Wann decimal statt float?

Drei Faustregeln:

  1. Geld und Mengen. Beträge in Euro/Cent, Lagerbestände, Inventur-Werte gehören in decimal(p, s) mit s ≥ 2.
  2. Regulierte Reporting-Werte. Werte, die in Abschlüssen, Steuer-Meldungen oder Behörden-Reports auftauchen, müssen reproduzierbar dezimal-exakt sein — float produziert je nach Berechnungsreihenfolge unterschiedliche Cent-Reste.
  3. Aggregations-tiefe Daten. Sobald ein Wert über Tausende Zeilen aggregiert wird (Summen, Durchschnitte über große Fakten-Tabellen), akkumulieren float-Rundungsfehler. decimal bleibt exakt.

Caveat: decimal ist nicht ausnahmslos exakt — bei der Konvertierung in eine niedrigere Skala rundet SQL Server still, ohne Fehler oder NULLTRY_CONVERT(decimal(10, 0), 1234.5) liefert 1235nicht NULL und auch keinen Fehler. Eine Konvertierung, die den Verlust von Nachkommastellen out-of-the-box als NULL signalisiert, gibt es in SQL Server nicht; nur SET NUMERIC_ROUNDABORT ON macht aus dem stillen Rundungsschritt einen Fehler (Default: OFF). Für eine vollständige Behandlung von decimal/numeric siehe TRY_CONVERT // Konvertierung nach decimal, numeric.

Postgres-Pendant für TRY_CONVERT(float, …)?

Es gibt kein direktes Pendant — CAST(s AS double precision) wirft Exception statt NULL, und auch Postgres 18 (Release 2025-09-25) hat weder eine eingebaute try_cast-Funktion noch die SQL/JSON CAST ... ON ERROR NULL-Syntax nachgezogen. Lösung daher (Stand 18.0): PL/pgSQL-Wrapper try_cast_double(p_input text) RETURNS double precision mit EXCEPTION WHEN invalid_text_representation OR numeric_value_out_of_range THEN RETURN NULL (siehe Postgres-Brücke). Da IEEE 754 plattform-übergreifend identisch ist, liefern try_cast_double und TRY_CONVERT(float, …) für denselben Input bit-identische Ergebnisse — Rundungsfehler eingeschlossen. Für formatierte Zahlen mit Tausender-/Dezimaltrennzeichen ist to_number(s, format) der Postgres-Weg, allerdings Locale-abhängig über lc_numeric.

Verwandte Artikel