Anyone who has watched a price import turn '123.45 €' into a NULL instead of the expected decimal number knows the drill: TRY_CONVERT(decimal(5, 2), '123,45') returns NULL, because a comma isn’t accepted as a decimal separator. And even with the comma gone, TRY_CONVERT(decimal(5, 2), '1234.56') is also NULL — this time because of one integer digit too many.
At a glance:
decimal(p, s)andnumeric(p, s)are functionally identical in SQL Server — this article usesdecimalthroughout, and every statement applies 1:1 tonumeric.TRY_CONVERTsilently rounds fractional digits tos, but rejects surplus integer digits withNULL.- In the text-to-
decimalconversion, a comma as decimal separator, an empty string, and scientific notation all yieldNULL. The two-stage pattern (text → float → decimal) catches the notation — as a parsing workaround with a precision limit, not as a universal route. - The Postgres counterpart is
numeric(p, s)ordecimal(p, s)(true synonyms).pis allowed up to 1000 (vs. 38 in SQL Server) — aTRY_CONVERTcounterpart is missing there, though, and the article shows the wrapper.
Prerequisite: TRY_CONVERT has existed since SQL Server 2012. The safe pattern uses TRIM and therefore needs SQL Server 2017+ (before that, use the LTRIM(RTRIM(…)) fallback). The Postgres examples require no particular version.
Content
- Decimal and Numeric — functionally identical
- Choosing precision and scale
- Rounding vs. integer overflow
- Converting text to decimal
- Converting typed numbers to decimal
- Safe type conversion
- Postgres bridge
- Summary
- FAQ
- Related Posts
Decimal and Numeric — functionally identical
SQL Server provides two data types for storing exact decimal numbers: decimal(p, s) and numeric(p, s). Both are synonyms with the same precision/scale rules and the same conversion behaviour. They are functionally identical — a column of type decimal(10, 2) and one of type numeric(10, 2) behave the same way in conversion and arithmetic.
This article uses decimal throughout. Every statement applies 1:1 to numeric.
Choosing precision and scale
decimal(p, s) has two parameters:
precision(p) — the total number of decimal digits the type can store. Allowed values are 1 to 38.scale(s) — the number of digits to the right of the decimal point. Allowed values are 0 top.
In concrete terms: decimal(5, 2) stores five digits in total — three before and two after the decimal point. That makes it immediately clear why 1234.56 fails and 123.456 gets rounded.
The value range follows from p and s:
decimal(p, s) | Min | Max | Typical use |
|---|---|---|---|
decimal(5, 2) | −999.99 | +999.99 | percentages, small prices |
decimal(10, 2) | −99,999,999.99 | +99,999,999.99 | accounting amounts |
decimal(19, 4) | −999,999,999,999,999.9999 | +999,999,999,999,999.9999 | money-like: same digit count, different value range (see TRY_CONVERT for money and smallmoney) |
decimal(38, 10) | (max form, 28 integer and 10 fractional digits) | (max form, 28 integer and 10 fractional digits) | financial mathematics, scientific values |
Rule of thumb: integer digits + fractional digits ≤ p. If a value has more integer digits than p − s permits, conversion is impossible — TRY_CONVERT then returns NULL, while a CAST/CONVERT raises an error. Fractional digits, on the other hand, are silently rounded without error.
Rounding vs. integer overflow
The asymmetry between fractional and integer digits is the central trap when working with decimal:
1: SELECT TRY_CONVERT(decimal(5, 2), '123.456') -- 123.46
2: SELECT TRY_CONVERT(decimal(5, 2), '1234.56') -- NULL
3: SELECT TRY_CONVERT(decimal(5, 2), '12345.6') -- NULL
4: SELECT TRY_CONVERT(decimal(5, 2), 1234.56) -- NULL
5: SELECT TRY_CONVERT(decimal(5, 2), 123.456) -- 123.46
- Fractional digits are rounded to
splaces — commercially, so a 5 rounds away from zero:10.645→10.65,-10.645→-10.65(half away from zero, not banker’s rounding). That happens silently, without error, withoutNULL. - Integer overflow yields
NULL— both for text input (lines 2, 3) and for typed numbers (line 4). TheNULLis specific toTRY_CONVERT: aCAST/CONVERTraises a runtime error at the same point.
Key point: A TRY_CONVERT(decimal(p, s), …) conversion can silently change the input value (fractional rounding) or silently signal the conversion with NULL (integer overflow). In ETL pipelines the latter is often the more dangerous trap: a NULL in the target column looks like “source delivered no value”, but actually means “value too large for the scale”.
Converting text to decimal
When a value of type nvarchar/varchar is passed to TRY_CONVERT, the input must represent a number. As the decimal separator, this text-to-decimal conversion accepts only the period. Thousands separators are not allowed. An empty string or a string consisting only of spaces converts directly to NULL — an important difference from float and money, where empty strings convert to 0.
1: SELECT TRY_CONVERT(decimal(5, 2), NULL ) -- NULL
2: SELECT TRY_CONVERT(decimal(5, 2), N'123' ) -- 123.00
3: SELECT TRY_CONVERT(decimal(5, 2), N'123,456' ) -- NULL
4: SELECT TRY_CONVERT(decimal(5, 2), N'123.456' ) -- 123.46
5: SELECT TRY_CONVERT(decimal(5, 2), N'' ) -- NULL
6: SELECT TRY_CONVERT(decimal(5, 2), N' ' ) -- NULL
7: SELECT TRY_CONVERT(decimal(5, 2), N' 123.456' ) -- 123.46
8: SELECT TRY_CONVERT(decimal(5, 2), N'123.456 ' ) -- 123.46
9: SELECT TRY_CONVERT(decimal(5, 2), N'1234.56' ) -- NULL
10: SELECT TRY_CONVERT(decimal(5, 2), N'123456E-3') -- NULL
What the lines show:
- Line 3:
'123,456'with a comma yieldsNULL— the comma is not recognised as decimal separator. Unlike Data quality in SQL Server // TRY_CONVERT for money and smallmoney done safely, where commas are ignored. - Lines 5, 6: an empty string and a string consisting only of spaces become
NULL. Different fromfloat,real,moneyandsmallmoney, where the result would be0. - Lines 7, 8: leading and trailing spaces are trimmed and don’t prevent conversion.
- Line 9:
'1234.56'has too many integer digits fordecimal(5, 2)(maxp − s = 3integer digits) →NULL. - Line 10: scientific notation
'123456E-3'as text returnsNULL. That changes when the number is passed as a typed value (see next section).
Converting typed numbers to decimal
When the number is already passed as a typed value (int, float, decimal), TRY_CONVERT can convert any numeric value to decimal — as long as the integer digits fit:
1: SELECT TRY_CONVERT(decimal(5, 2), NULL ) -- NULL
2: SELECT TRY_CONVERT(decimal(5, 2), 123 ) -- 123.00
3: SELECT TRY_CONVERT(decimal(5, 2), 123.456 ) -- 123.46
4: SELECT TRY_CONVERT(decimal(5, 2), 1234.56 ) -- NULL
5: SELECT TRY_CONVERT(decimal(5, 2), 123456E-3) -- 123.46
What the lines show:
- Line 3: a typed decimal is rounded to
s(123.456→123.46). - Line 4: the decimal literal
1234.56— typed by T-SQL asnumeric(6, 2)— exceeds thep − s = 3integer-digit budget →NULL. - Line 5: scientific notation does work as a typed number (
123456E-3→123.456→ rounded to123.46). This difference from the text variant is the basis for the two-stage pattern in the next section.
Safe type conversion
“Safe” in this series means: fault-tolerant — no abort, NULL instead of a runtime error. It does not automatically mean lossless. That is exactly why the limitations below belong to this pattern.
The sections above showed two edge cases that have to be handled explicitly in the import path:
- Scientific notation as text (
'12345E-3') yieldsNULL. Workaround: convert tofloatfirst (which tolerates the notation), then todecimal(which enforces precision and scale). - An empty string is converted to
NULLdirectly byTRY_CONVERT(decimal, …)— but once the two-stage pattern routes throughfloat, that no longer holds:TRY_CONVERT(float, '')yields0. The empty string therefore has to be mapped toNULLexplicitly before thefloatstep.
The following example handles both edge cases together — as a parsing workaround, not a universal route (the limitations follow right below the example):
1: DECLARE @p_input AS nvarchar(30);
2: SET @p_input = N'123,456';
3:
4: SELECT TRY_CONVERT( decimal(5, 2)
5: , TRY_CONVERT( float
6: , REPLACE( CASE WHEN TRIM(@p_input) = ''
7: THEN NULL
8: ELSE @p_input
9: END
10: , ','
11: , '.'
12: )
13: )
14: ) AS [Output]; -- 123.46
Two limitations are part of this pattern:
- Input grammar. The pattern normalises only the decimal separator (comma → period). Thousands separators (
'1.234,56') and currency symbols — like the€from the opening example — have to be removed by an upstream cleansing step, otherwise the result staysNULL. Whitespace is narrowly defined too: without further arguments,TRIMremoves only regular spaces, not tabs or other Unicode whitespace. - Precision limit.
float(without parametersfloat(53), IEEE 754 double precision) is an approximate data type with 15–17 significant decimal digits — not every decimal number can be represented exactly. For the values shown in this article, the detour produces the samedecimal(5, 2)result as a direct conversion. For monetary amounts and other business-exact values with more significant digits, however, thefloatintermediate step can change the value. When exactness matters and no scientific notation occurs, the direct routeTRY_CONVERT(decimal(p, s), …)without the intermediate step is the better choice.
If the pattern is needed across many columns in an ETL flow, abstract it into a user-defined function fn_try_convert_decimal(@p_input nvarchar, @p_precision int, @p_scale int) — see Design Pattern // Safe Type Conversion with T-SQL.
Postgres bridge
For multi-engine pipelines, decimal is the easiest bridge in the entire TRY_CONVERT cluster: numeric(p, s) and decimal(p, s) fully cover the SQL standard in Postgres and extend it in a few places.
Three differences against SQL Server:
- Synonyms, not just identical. In Postgres,
numeric(p, s)anddecimal(p, s)are true synonyms — the column definition is always stored asnumericinternally. In SQL Server the original identifier is preserved. - Declared
precisionup to 1000. Explicitly declarednumeric(p, s)types allowpup to 1000 (vs. 38 in SQL Server). Since Postgres 15, the declaredscalemay even be negative (−1000 to 1000) — it then rounds to digits left of the decimal point. An unconstrainednumericwithout parameters is subject to much larger implementation limits: up to 131,072 integer digits and 16,383 fractional digits. - Scientific notation as text works. Postgres
CAST('123456E-3' AS numeric(5, 2))yields123.46. The two-stage pattern viafloatis therefore not needed in Postgres — the notation is accepted directly.
Postgres, however, has no built-in try_cast (not in Postgres 18 either, released 2025-09-25). A PL/pgSQL wrapper provides the NULL-instead-of-exception behaviour:
1: CREATE OR REPLACE FUNCTION fn_try_cast_numeric_5_2
2: (
3: IN p_input text
4: )
5: RETURNS numeric(5, 2)
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(REPLACE(p_input, ',', '.') AS numeric(5, 2));
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_numeric_5_2('123,456'); -- 123.46
24: SELECT fn_try_cast_numeric_5_2('123456E-3'); -- 123.46
25: SELECT fn_try_cast_numeric_5_2('1234.56'); -- NULL
26: SELECT fn_try_cast_numeric_5_2(''); -- NULL
The EXCEPTION block deliberately catches the two expected error classes — invalid number representation (invalid_text_representation) and value-range overflow (numeric_value_out_of_range). A blanket WHEN OTHERS would silently turn unexpected errors into NULL as well. One cost note on top: according to the Postgres documentation, a block with an EXCEPTION clause is significantly more expensive than one without. For large data volumes with many expected errors, set-based pre-checks are worth a look.
Summary
decimal(p, s)andnumeric(p, s)are functionally identical in SQL Server — this article usesdecimal, and every statement applies 1:1 tonumeric.TRY_CONVERT(decimal(p, s), …)silently rounds fractional digits, but rejects surplus integer digits withNULL. The asymmetry is the central trap in ETL pipelines.- For text inputs: comma →
NULL, empty string →NULL, scientific notation →NULL. For typed numbers, scientific notation works. Hence the two-stage pattern (text → float → decimal) — as a parsing workaround with afloatprecision limit, not for exact values with many significant digits. - Postgres counterpart:
numeric(p, s)ordecimal(p, s)— same value ranges up top = 38, beyond that up top = 1000. Scientific notation as text is accepted directly, so the two-stage pattern is not needed there.
As a practical decision aid:
| Input situation | Recommended route |
|---|---|
| Period notation, scale known | direct TRY_CONVERT(decimal(p, s), …) |
| Comma as decimal separator | normalise the decimal separator, then TRY_CONVERT |
| Scientific notation as text | two-stage pattern — only if the precision risk is acceptable |
| Monetary amounts and other business-exact values | no float intermediate step — validate the input upstream |
| ETL error diagnosis | store raw and converted value separately, log the error class |
FAQ
decimal or numeric — which one? Both are synonyms in SQL Server and functionally identical. Which identifier you use is a matter of convention: decimal is more common in T-SQL communities, numeric in ANSI standard documentation. Tools like the SSMS designer often default to decimal.
TRY_CONVERT(decimal(5, 2), '123,45') return NULL? Because the comma is not recognised as a decimal separator in SQL Server — TRY_CONVERT expects only a period. For German-format input data, replace the comma with a period before conversion. The safe conversion pattern above handles that with REPLACE.
TRY_CONVERT(decimal, …) round or reject? Both — depending on whether the problem sits on the fractional side or the integer side. Fractional digits are silently rounded to s (half away from zero). Integer overflow yields NULL. This asymmetry matters in ETL pipelines: a NULL in the target column for decimal doesn’t necessarily mean “source delivered no value” — it can also mean “source had too many integer digits”.
TRY_CONVERT(decimal, …) return NULL even though the number looks valid? Usually integer overflow is behind it: the number is syntactically correct but has more than p − s integer digits. A counter-check with a larger precision separates the cases. If TRY_CONVERT(decimal(38, 2), …) returns a value, it was overflow. If the counter-check stays NULL as well, the number representation itself is invalid. For ETL checks it pays to store the raw and the converted value separately and to log the error class alongside.
'12345E-3') to decimal? The two-stage pattern from the “Safe type conversion” section — first to float (which tolerates the notation as text), then to decimal (which enforces precision/scale). The float intermediate step is a parsing workaround: with more than roughly 15 significant digits it can change the value (see the limitations in that section). Cross-reference: Data quality in SQL Server // TRY_CONVERT for float and real done safely.
TRY_CAST or TRY_CONVERT for decimal? Both provide the NULL-instead-of-error behaviour and are interchangeable for this use case. TRY_CONVERT additionally offers the optional style parameter — it plays no role for decimal, but it does for date and money conversions. If you want to stay close to the SQL standard, use TRY_CAST. All four functions are compared in Data Quality // Type Conversion Basics with T-SQL.
TRY_CONVERT(decimal, …)? CAST(s AS numeric(p, s)) or CAST(s AS decimal(p, s)) — Postgres makes no distinction. For NULL-instead-of-exception behaviour you need a PL/pgSQL wrapper (Postgres has no built-in try_cast as of 18.0). Postgres is also more liberal with text inputs: scientific notation is accepted directly.
Related Posts
ETL context:
- Data quality in an ETL process
- ETL vs. ELT — How to Tell Which Pattern You Actually Built — the macro view: ETL vs. ELT as an architecture decision, not the order of the letters.
TRY_CONVERT for other data types:
- Data quality in SQL Server // TRY_CONVERT for date, datetime, datetime2 and time done safely
- Data quality in SQL Server // TRY_CONVERT for bigint, int, smallint and tinyint done safely
- Data quality in SQL Server // TRY_CONVERT for money and smallmoney done safely
- Data quality in SQL Server // TRY_CONVERT for float and real done safely
- Data quality in SQL Server // TRY_CONVERT for bit done safely — converting yes/no values
Fundamentals:
- Data Quality // Type Conversion Basics with T-SQL — CAST, CONVERT, TRY_CAST and TRY_CONVERT compared.