Datenqualität in SQL Server // TRY_CONVERT für bigint, int, smallint und tinyint sicher anwenden

Ein CSV-Import läuft ohne Fehlermeldung durch, und hinterher steht in der Mengen-Spalte eine 0, wo das Quellfeld schlicht leer war: TRY_CONVERT(int, N'') liefert 0, nicht NULL. Die zweite Eigenheit betrifft bereits typisierte Dezimalzahlen: TRY_CONVERT(int, 1234.5) rundet nicht, sondern schneidet ab — das Ergebnis ist 1234, nicht 1235.

Auf einen Blick:

  • Die vier Integer-Typen von SQL Server decken klar abgestufte Wertebereiche ab — vom 1-Byte-tinyint (0..255, als einziger unsigned) bis zum 8-Byte-bigint. Bei der Konvertierung aus demselben Quelltyp verhalten sich alle vier gleich.
  • Aus Text konvertiert TRY_CONVERT nur ganzzahlige Darstellungen. Vorzeichen und umgebende Leerzeichen sind erlaubt, Dezimal- und Tausendertrennzeichen ergeben Locale-unabhängig NULL.
  • Zwei stille Sonderfälle: Eine leere Zeichenfolge wird zu 0 konvertiert (im Import meist falsch), und typisierte Dezimalzahlen werden trunkiert statt gerundet. Das sichere Pattern behandelt beide.
  • In Postgres wirft CAST(… AS bigint) bei ungültigem Input eine Exception — ein eingebautes Try-Pendant fehlt. Der Artikel zeigt den PL/pgSQL-Wrapper fn_try_cast_bigint.

Voraussetzung: TRY_CONVERT gibt es seit SQL Server 2012. Das sichere Pattern verwendet zusätzlich TRIM und braucht damit SQL Server 2017+ (davor LTRIM(RTRIM(…))). Die Beispiele laufen ohne Sample-Datenbank mit reinen Inline-Literalen, die Postgres-Seite setzt keine bestimmte Version voraus.

Inhalt

Wertbereich der Integer-Typen

Vor der Konvertierung steht die Typ-Wahl. Die vier Integer-Typen in SQL Server unterscheiden sich durch Wertebereich und Speicherbedarf. Für Konvertierungen aus demselben Quelltyp gelten bei allen vier Ziel-Typen dieselben Regeln — entscheidend ist, ob der Wert in den Ziel-Wertebereich passt.

DatentypMinMaxBytesTypische Verwendung
bigint-9 223 372 036 854 775 8089 223 372 036 854 775 8078Riesige Surrogate-Keys, Counter über Milliarden, globale 64-Bit-IDs
int-2 147 483 6482 147 483 6474Standard-Surrogate-Key, Mengen, Counter im 9-stelligen Bereich
smallint-32 76832 7672Jahre, Klein-Mengen, ältere Lookup-Schlüssel
tinyint02551Flags, Status-Codes, kleine Lookup-Werte (unsigned)

tinyint ist eine Microsoft-Eigenheit und der einzige SQL-Server-Integer-Typ ohne Vorzeichen. Ein negativer Wert lässt sich nicht in tinyint konvertieren, der TRY_CONVERT-Aufruf gibt NULL zurück. Postgres kennt diesen Typ gar nicht — kleinster Integer dort ist smallint (signed, -32 768..32 767).

Text nach Integer konvertieren

Text-Eingangswerte kommen typischerweise aus CSV-, JSON- und XML-Importen. Wird ein solcher nvarchar-/varchar-Wert an TRY_CONVERT übergeben, muss der Text eine ganze Zahl darstellen. Erlaubt sind Ziffern, ein optionales Vorzeichen und umgebende Leerzeichen. Ein Dezimal- oder Tausendertrennzeichen beendet die Konvertierung dagegen mit NULL, und eine leere Zeichenfolge wird zu 0 konvertiert. Das ist ein wichtiger Unterschied zu TRY_CONVERT(decimal(18, 2), N''), das NULL liefert.

  1: SELECT TRY_CONVERT(int, NULL          ) -- NULL
  2: SELECT TRY_CONVERT(int, N'123'        ) -- 123
  3: SELECT TRY_CONVERT(int, N'123,4'      ) -- NULL
  4: SELECT TRY_CONVERT(int, N'1,234'      ) -- NULL
  5: SELECT TRY_CONVERT(int, N'123.4'      ) -- NULL
  6: SELECT TRY_CONVERT(int, N'1.234'      ) -- NULL
  7: SELECT TRY_CONVERT(int, N''           ) -- 0
  8: SELECT TRY_CONVERT(int, N' '          ) -- 0
  9: SELECT TRY_CONVERT(int, N' 123'       ) -- 123
 10: SELECT TRY_CONVERT(int, N'123 '       ) -- 123
 11: SELECT TRY_CONVERT(int, N'2147483648' ) -- NULL
 12: SELECT TRY_CONVERT(int, N'123456E-3'  ) -- NULL

Die Zeilen zeigen im Einzelnen:

  • Zeile 2: Eine reine Ziffernfolge wird wie erwartet konvertiert.
  • Zeilen 3–6: Bei der Text-nach-Integer-Konvertierung scheitern Komma und Punkt in jeder Rolle, als Dezimal- wie als Tausendertrennzeichen. Diese Konvertierung ist Locale-unabhängig: Weder SET LANGUAGE noch SET DATEFORMAT ändern etwas an diesem Ergebnis.
  • Zeilen 7, 8: Eine leere Zeichenfolge und ein String nur aus Leerzeichen werden zu 0 konvertiert, nicht zu NULL.
  • Zeilen 9, 10: Führende und folgende Leerzeichen sind erlaubt und ändern das Ergebnis nicht.
  • Zeile 11: Der Wert liegt um 1 über dem int-Maximum von 2 147 483 647TRY_CONVERT behandelt den Overflow als fehlgeschlagene Konvertierung und liefert NULL.
  • Zeile 12: Wissenschaftliche Notation wird als Text nicht akzeptiert. Als typisierte Zahl funktioniert sie, das zeigt die nächste Sektion.

Kernaussage: Ein leeres Feld ist im Import semantisch ein „unbekannt“, TRY_CONVERT macht daraus aber den fachlichen Wert 0: still, ohne Fehler, ohne NULL. Wo das falsch ist (der Regelfall bei CSV-Importen), mappt das Pattern aus „Sichere Typ-Konvertierung“ die leere Zeichenfolge vorab auf NULL.

Typisierte Zahlen nach Integer konvertieren

Kommt der Eingangswert bereits typisiert an, etwa als Ganzzahl mit anderem Wertebereich oder als Dezimalzahl aus einer Pipeline-Berechnung, reduzieren sich die Szenarien auf zwei Fragen: Passt der Wert in den Ziel-Wertebereich? Und was passiert mit Nachkommastellen?

  1: SELECT TRY_CONVERT(int, 2147483648) -- NULL
  2: SELECT TRY_CONVERT(int,        123) -- 123
  3: SELECT TRY_CONVERT(int,     1234.5) -- 1234
  4: SELECT TRY_CONVERT(int,  123456E-3) -- 123

Die Zeilen zeigen im Einzelnen:

  • Zeile 1: 2147483648 liegt um 1 über dem int-Maximum, die Konvertierung schlägt fehl und liefert NULL.
  • Zeile 3: Die typisierte Dezimalzahl wird zum ganzzahligen Anteil abgeschnitten, aus 1234.5 wird 1234. Wer Rundung will, schaltet sie explizit vor: ROUND(1234.5, 0) liefert 1235.0, das anschließend zu 1235 konvertiert.
  • Zeile 4: Wissenschaftliche Notation funktioniert als typisiertes float-Literal: 123456E-3 ist 123.456, trunkiert 123. Als Text (Zeile 12 der vorigen Sektion) ergibt dieselbe Notation NULL.

Kernaussage: TRY_CONVERT nach Integer trunkiert die Nachkommastellen von decimal– und float-Werten stillschweigend — anders als die Konvertierung nach decimal(p, s), die kaufmännisch rundet (half away from zero). Eine dokumentierte Ausnahme ist money als Quelltyp, das SQL Server beim Integer-Ziel rundet. Overflow quittiert TRY_CONVERT mit NULL, wo CAST und CONVERT einen Laufzeitfehler auslösen.

Die Wertebereichs-Grenzen pro Typ zeigt ein zweiter Block:

  1: SELECT TRY_CONVERT(smallint,  32767) -- 32767  (smallint-MAX)
  2: SELECT TRY_CONVERT(smallint,  32768) -- NULL   (Overflow)
  3: SELECT TRY_CONVERT(tinyint,     255) -- 255    (tinyint-MAX, unsigned)
  4: SELECT TRY_CONVERT(tinyint,     256) -- NULL   (Overflow)
  5: SELECT TRY_CONVERT(tinyint,      -1) -- NULL   (tinyint ist unsigned)

Bei tinyint schlägt die Konvertierung in beide Richtungen fehl: 256 ist zu groß, -1 verletzt die Unsigned-Grenze.

Sichere Typ-Konvertierung

„Sicher“ meint in dieser Serie fehlertolerant: kein Abbruch, NULL statt Laufzeitfehler. Verlustfrei ist damit nicht automatisch gemeint — bei den Integer-Typen gehört die stille Trunkierung ausdrücklich zu den Verlusten, die das Pattern nicht verhindert. Auch semantisch falsche Werte (wie die 0 aus einer leeren Zeichenfolge) verhindert TRY_CONVERT von sich aus nicht, genau dafür gibt es das Pattern unten. Und grundsätzlich unzulässige Typ-Kombinationen fängt die Funktion ebenfalls nicht ab: Sie lösen weiterhin einen Fehler aus.

Zwei Sonderfälle aus den Sektionen oben brauchen im Import-Pfad eine explizite Behandlung:

  • Leere Zeichenfolge → 0: Wenn das fachlich falsch ist (der Regelfall bei CSV-Importen), muss die leere Zeichenfolge vor dem TRY_CONVERT auf NULL gemappt werden.
  • Typisierte Dezimalzahl → Trunkierung: Falls Rundung gewünscht ist, kommt vor das TRY_CONVERT ein ROUND(…, 0). Die Alternative decimal(p, 0) als Ziel-Typ rundet zwar, tut das aber ebenfalls still (siehe FAQ).

Das Anwendungs-Beispiel mit DECLARE und snake_case-Variablen-Konvention löst den ersten Sonderfall:

  1: DECLARE @p_input AS nvarchar(30);
  2: SET @p_input = N'123';
  3: 
  4: SELECT TRY_CONVERT( int
  5:                   , CASE WHEN TRIM(@p_input) = '' THEN NULL ELSE @p_input END
  6:                   ) AS [Output];

Zwei Einschränkungen gehören zu dem Pattern dazu:

  • Eingabegrammatik: Das Pattern fängt nur die leere Zeichenfolge ab. Tausendertrennzeichen und Währungszeichen muss eine vorgelagerte Bereinigung entfernen, sonst bleibt das Ergebnis NULL. Und TRIM entfernt ohne weitere Angabe nur das normale Leerzeichen (char(32)) — einen String aus Tabs oder anderem Unicode-Leerraum erkennt das Pattern deshalb nicht als „leer“. Können solche Zeichen in den Quelldaten vorkommen, muss die Normalisierung sie explizit berücksichtigen.
  • Keine Rundung: Bei der Konvertierung in einen Integer-Zieltyp wird der Nachkommateil abgeschnitten, das Pattern ändert daran nichts. Soll gerundet werden, kommt die Rundung explizit als ROUND(…, 0) vor das TRY_CONVERT.

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

Postgres-Brücke

Auf der Postgres-Seite ist die Typ-Frage schnell beantwortet: smallintinteger und bigint decken dieselben Wertebereiche ab wie ihre SQL-Server-Pendants, nur tinyint fehlt (eine tinyint-Spalte wird bei einer Migration üblicherweise auf smallint abgebildet). Der eigentliche Unterschied liegt im Fehlerverhalten:

  • CAST(… AS bigint) wirft bei ungültigem Input eine Exception: Ein eingebautes Try-Pendant gibt es nicht — auch Postgres 18 (Release 2025-09-25) bringt weder eine try_cast-Funktion noch die SQL/JSON-Syntax CAST … ON ERROR NULL mit.
  • Auch die leere Zeichenfolge wirft eine Exception: CAST('' AS bigint) scheitert mit invalid_text_representation. Das ist strenger als T-SQL, wo TRY_CONVERT(int, N'') still 0 liefert.

Das NULL-statt-Exception-Verhalten liefert ein PL/pgSQL-Wrapper. Er übernimmt dabei gleich die sichere Semantik des Patterns oben: Die leere Zeichenfolge wird zu NULL gemappt, nicht zu 0:

  1: CREATE OR REPLACE FUNCTION fn_try_cast_bigint
  2: (
  3:     IN    p_input              text
  4: )
  5: RETURNS bigint
  6: LANGUAGE plpgsql
  7: IMMUTABLE
  8: AS $function$
  9: BEGIN
 10: 
 11:    IF p_input IS NULL OR TRIM(p_input) = '' THEN
 12:       RETURN NULL;
 13:    END IF;
 14: 
 15:    RETURN CAST(p_input AS bigint);
 16: 
 17: EXCEPTION
 18:    WHEN invalid_text_representation OR numeric_value_out_of_range THEN
 19:       RETURN NULL;
 20: END;
 21: $function$;
 22: 
 23: SELECT fn_try_cast_bigint('123');      -- 123
 24: SELECT fn_try_cast_bigint(' 123 ');    -- 123
 25: SELECT fn_try_cast_bigint('1,234');    -- NULL
 26: SELECT fn_try_cast_bigint('');         -- NULL

Damit verhält sich fn_try_cast_bigint('1,234') wie TRY_CONVERT(int, '1,234'): Der Aufruf gibt NULL zurück, statt eine Exception zu werfen. Für int und smallint lassen sich analoge Wrapper mit angepasstem Rückgabe-Typ definieren.

Der EXCEPTION-Block fängt gezielt die zwei Fehlerklassen, die bei der Integer-Konvertierung erwartbar sind: invalid_text_representation für ungültige Zahl-Darstellungen und numeric_value_out_of_range für Overflow. Ein pauschales WHEN OTHERS würde auch unerwartete Fehler still zu NULL machen. Kostenlos ist der EXCEPTION-Teil allerdings nicht — die Postgres-Doku stuft einen Block mit Exception-Handler als deutlich teurer ein als einen ohne. Bei Massen-Imports mit vielen erwartbaren Fehl-Werten lohnt es sich deshalb, set-basiert vorzuprüfen, welche Werte konvertierbar sind, und nur diese zu casten. Seit Postgres 16 gibt es dafür passendes Werkzeug: pg_input_is_valid('123', 'bigint') prüft die Konvertierbarkeit, ohne einen fehlschlagenden Cast auszulösen, und pg_input_error_info(…) liefert die Fehlerdetails dazu. Der Wrapper bleibt das Komfort-Pattern für die wiederverwendbare Einzelwert-Konvertierung.

Liefert die Quelle formatierte Zahlen mit Tausendertrennzeichen, übernimmt to_number mit einem Format-Pattern das Parsen:

  1: SELECT to_number('1.234', 'FM999G999')  -- 1234  (deutsche Notation, lc_numeric = 'de_DE.UTF-8')
  2: SELECT to_number('1,234', 'FM999G999')  -- 1234  (US-Notation, lc_numeric = 'en_US.UTF-8')

Locale-abhängig ist to_number über die Format-Zeichen G (Tausendertrenner) und D (Dezimaltrenner): Beide richten sich nach der Session-Variable lc_numeric, während ein literales . oder , im Format-Pattern Locale-unabhängig bleibt. Damit unterscheidet sich to_number von der Text-nach-Integer-Konvertierung in SQL Server, für die das Session-Locale keine Rolle spielt. Die Locale-Namen in den Kommentaren sind plattformabhängig, de_DE.UTF-8 und en_US.UTF-8 dienen der Illustration. Für reproduzierbare ETL-Pipelines sollte das Zahlenformat deshalb nie implizit an der Server-Konfiguration hängen: entweder lc_numeric pro Session explizit setzen oder die Eingabe vorab so normalisieren, dass das Format-Pattern ohne Locale-Zeichen auskommt. Der Rückgabe-Typ von to_number ist außerdem numeric, für eine Integer-Spalte folgt also noch ein CAST.

Zusammenfassung

  • Die vier Integer-Typen unterscheiden sich beim Konvertieren vor allem im Wertebereich. Typ-Wahl: int als Default, bigint bei nachweislich größerem Wertebereich, smallint und tinyint, wenn ihr Wertebereich fachlich dauerhaft ausreicht und die geringere Breite ins Gewicht fällt.
  • Aus Text akzeptiert TRY_CONVERT bei Integer-Zielen nur ganzzahlige Darstellungen (optional Vorzeichen und umgebende Leerzeichen) — Trennzeichen, wissenschaftliche Notation und Overflow ergeben NULL, unabhängig vom Session-Locale.
  • Die zwei stillen Fallen: Eine leere Zeichenfolge wird zu 0 konvertiert, typisierte Dezimalzahlen werden trunkiert. Das sichere Pattern mappt die leere Zeichenfolge per CASE/TRIM auf NULL, Rundung kommt explizit als ROUND(…, 0) davor.
  • Postgres hat kein eingebautes Try-Pendant: Der Wrapper fn_try_cast_bigint liefert NULL statt Exception, to_number parst formatierte Zahlen (Locale-abhängig über lc_numeric).

Für die Praxis als Entscheidungshilfe:

EingangslageEmpfohlener Weg
Reine Ziffernfolge, optional mit Vorzeichendirekt TRY_CONVERT(int, …)
Leere Zeichenfolgen möglich (CSV-Import)sicheres Pattern: CASE/TRIM mappt auf NULL, dann TRY_CONVERT
Dezimalzahlen, Rundung gewünschtROUND(…, 0) vor dem TRY_CONVERT
Tausendertrennzeichen, Quell-Locale bekanntgezielt normalisieren (REPLACE), in Postgres to_number
Quell-Locale unbekannt oder gemischtnicht blind normalisieren — als Datenqualitätsfehler zurückweisen
NULL unklarer Herkunft im Ziel-FeldGegenprobe mit TRY_CONVERT(bigint, …)int-Overflow eingrenzen (siehe FAQ)

FAQ

Warum wird TRY_CONVERT(int, N'') zu 0 statt NULL?

SQL Server behandelt die leere Zeichenfolge bei der Text-nach-Integer-Konvertierung wie eine 0 — die Konvertierung gilt als erfolgreich, es gibt weder Fehler noch NULL. Dasselbe gilt für Strings, die nur aus Leerzeichen bestehen. Im Import ist das meist nicht gemeint, denn ein leeres CSV-Feld steht für „unbekannt“. Das sichere Pattern aus „Sichere Typ-Konvertierung“ mappt die leere Zeichenfolge deshalb per CASE/TRIM vorab auf NULL.

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

Weil die Text-nach-Integer-Konvertierung Locale-unabhängig arbeitet und weder Dezimal- noch Tausendertrennzeichen kennt. Erwartet wird eine reine Ziffernfolge, optional mit Vorzeichen und umgebenden Leerzeichen. Sobald ein Komma oder ein Punkt im String steht, scheitert die Konvertierung, unabhängig von SET LANGUAGE oder SET DATEFORMAT. Liefert die Quelle verlässlich deutsche Notation ('1.234,56'), hilft eine REPLACE-Bereinigung: REPLACE(REPLACE(@p_input, '.', ''), ',', '.') macht daraus '1234.56', das anschließend per TRY_CONVERT(decimal(18, 2), …) konvertiert.

int oder bigint — welcher Typ wann?

int reicht für jeden Wertebereich bis rund ±2,1 Milliarden und ist der Default für Surrogate-Keys, Counter und Mengen. bigint kommt zum Einsatz, sobald der Wertebereich nachweislich darüber hinauswächst, etwa bei globalen 64-Bit-ID-Generatoren. smallint und tinyint sind sinnvoll, wenn ihr Wertebereich fachlich dauerhaft ausreicht und die geringere Speicherbreite bei Tabelle oder Indizes ins Gewicht fällt. Wer dagegen nur auf Verdacht verkleinert, bezahlt beim Schema-Wachstum mit dem Overflow-Risiko.

Wie erzwingt man Rundung statt Trunkierung beim Konvertieren von 1234.5?

TRY_CONVERT(int, 1234.5) liefert 1234, also Trunkierung. Für kaufmännische Rundung wird vorher explizit ROUND eingeschoben: TRY_CONVERT(int, ROUND(1234.5, 0)) liefert 1235. Achtung: decimal(p, 0) ist kein strikter Ersatz für int — SQL Server rundet bei der Skala-Reduktion still (TRY_CONVERT(decimal(10, 0), 1234.5) liefert 1235, keinen Fehler und kein NULL). Eine Konvertierung, die den Verlust von Nachkommastellen von sich aus als NULL signalisiert, gibt es in SQL Server nicht. Nur SET NUMERIC_ROUNDABORT ON macht aus dem stillen Rundungsschritt einen Fehler (Default: OFF).

Was tun, wenn die CSV-Spalte gemischt Komma und Punkt als Dezimaltrennzeichen enthält?

Zuerst klären, ob das Quell-Locale bekannt ist — nur dann ist Normalisieren zulässig. Steht fest, dass das Komma Dezimaltrennzeichen und der Punkt Tausendertrennzeichen ist (deutsche Notation), normalisiert REPLACE(REPLACE(@p_input, '.', ''), ',', '.') vor dem TRY_CONVERT. Liefert die Spalte beide Notationen gemischt, lässt sich das nicht zuverlässig automatisch unterscheiden, denn '1,234' ist mehrdeutig. Solche Werte sind ein Datenqualitätsfehler der Eingangs-Datenquelle: zurückweisen und protokollieren statt raten. Im Zweifel pro Datensatz dokumentieren, welches Locale die Quelle nutzt.

Warum liefert TRY_CONVERT(int, …) NULL, obwohl die Zahl gültig aussieht?

Die zwei häufigsten Ursachen sind Overflow und unsichtbare Zeichen. Eine Gegenprobe trennt die Fälle: Liefert TRY_CONVERT(bigint, …) plötzlich einen Wert, war es int-Overflow. Bleibt auch die Gegenprobe NULL, kommen zwei Ursachen infrage: eine ungültige Darstellung (verstecktes Trennzeichen, wissenschaftliche Notation oder Leerraum-Zeichen jenseits des normalen Leerzeichens char(32), etwa Tabs oder geschützte Leerzeichen, die das einfache TRIM nicht entfernt) oder ein Wert, der selbst den bigint-Wertebereich sprengt.

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

Ein direktes Pendant gibt es nicht: CAST(s AS int) wirft bei ungültigem Input eine Exception, und ein eingebautes try_cast fehlt auch in Postgres 18. Der Weg ist ein PL/pgSQL-Wrapper wie fn_try_cast_bigint aus der Postgres-Brücke, der die Fehlerklassen invalid_text_representation und numeric_value_out_of_range gezielt zu NULL macht. Für formatierte Zahlen mit Tausendertrennzeichen ist to_number(s, format) der Postgres-Weg — Locale-abhängig über lc_numeric, anders als in SQL Server, wo das Session-Locale für die Integer-Konvertierung keine Rolle spielt.

Verwandte Artikel

ETL-Kontext:

TRY_CONVERT für andere Datentypen:

Grundlagen: