37

我想将一varchar(max)列转换为decimal(10,4).

当我尝试使用castconvert遇到算术溢出异常时。问题是存储在 varchar 列中的数据可能包含不同的精度和不同的比例。例如,123456789.1234567'、1.12345678 或 123456.1234。

对于像 123456.1234 这样的值,它正在转换没有任何问题,但对于其他值,我遇到了一些问题。

4

12 回答 12

30

经过测试,我发现问题不是小数位,而是精度(10)

这不起作用:将 varchar 转换为数字数据类型的算术溢出错误。

DECLARE @TestConvert VARCHAR(MAX) = '123456789.12343594'

SELECT CAST(@TestConvert AS DECIMAL(10, 4))

这有效

DECLARE @TestConvert VARCHAR(MAX) = '123456789.12343594'

SELECT CAST(@TestConvert AS DECIMAL(13, 4))

应该像 9 int + 4 floating = 13 chars

于 2012-06-18T21:55:36.103 回答
7

我的解释在代码中。:)

DECLARE @TestConvert VARCHAR(MAX) = '123456789.1234567'
BEGIN TRY
    SELECT CAST(@TestConvert AS DECIMAL(10, 4))
END TRY
BEGIN CATCH
    SELECT 'The reason you get the message "' + ERROR_MESSAGE() + '" is because DECIMAL(10, 4) only allows for 4 numbers after the decimal.'
END CATCH

-- Here's one way to truncate the string to a castable value.
SELECT CAST(LEFT(@TestConvert, (CHARINDEX('.', @TestConvert, 1) + 4)) AS DECIMAL(14, 4))

-- If you noticed, I changed it to DECIMAL(14, 4) instead of DECIMAL(10, 4) That's because this number has 14 digits, as proven below.
-- Read this for a better explanation as to what precision, scale and length mean: http://msdn.microsoft.com/en-us/library/ms190476(v=sql.105).aspx
SELECT LEN(LEFT(@TestConvert, (CHARINDEX('.', @TestConvert, 1) + 4)))
于 2012-06-18T21:33:43.113 回答
6

我想出了以下解决方案:

SELECT [Str], DecimalParsed = CASE 
WHEN ISNUMERIC([Str]) = 1 AND CHARINDEX('.', [Str])=0 AND LEN(REPLACE(REPLACE([Str], '-', ''), '+', '')) < 29 THEN CONVERT(decimal(38,10), [Str])
WHEN ISNUMERIC([Str]) = 1 AND (CHARINDEX('.', [Str])!=0 AND CHARINDEX('.', REPLACE(REPLACE([Str], '-', ''), '+', ''))<=29) THEN 
    CONVERT(decimal(38,10), 
            CASE WHEN LEN([Str]) - LEN(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE([Str], '0', ''), '1', ''), '2', ''), '3', ''), '4', ''), '5', ''), '6', ''), '7', ''), '8', ''), '9', '')) <= 38 
                 THEN [Str] 
                 ELSE SUBSTRING([Str], 1, 38 + LEN(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE([Str], '0', ''), '1', ''), '2', ''), '3', ''), '4', ''), '5', ''), '6', ''), '7', ''), '8', ''), '9', ''))) END)
ELSE NULL END
FROM TestStrToDecimal

我知道这看起来有点矫枉过正,可能确实是这样,但它对我有用(检查了不同精度和比例的正数、负数、大数和小数 - 一切都转换为decimal(38,10)or NULL)。

它是硬编码的decimal(38,10),因此如果您需要不同的精度,请更改代码中的常量(38、10、29)。

这个怎么运作?结果是

  • 如果转换很简单,没有溢出或精度损失(例如 123 或 123.456),那么它只是转换它。
  • 如果数字不是太大,但小数点后的数字太多(例如 123.1234567890123456789012345678901234567890),那么它会在最后修剪超出的数字,只保留前 38 个数字。
  • 如果数字太大并且不能在没有溢出的情况下转换为十进制(例如 9876543210987654321098765432109876543210),则返回 NULL

在上面的代码中,每种情况都是单独的 WHEN 语句。

以下是几个转换示例: 在此处输入图像描述

于 2014-04-28T17:20:25.910 回答
5

您错过了 6.999,50 不是有效小数的事实。你肯定不能在十进制值中有逗号和小数点吗?它应该是什么数字?

假设您的语言环境指定 . 作为分组和 , 作为小数分隔符:删除分组数字:

选择转换(十进制(11,2),替换('6.999,50','.',''))

将产生 6999,50 作为小数

于 2013-08-25T05:24:57.327 回答
5

你还没有解释为什么你不能使用 Float 数据类型,所以这里有一个例子:

DECLARE @StringVal varchar(50)

SET @StringVal = '123456789.1234567'
SELECT @StringVal, CAST(@StringVal AS FLOAT)

SET @StringVal = '1.12345678'
SELECT @StringVal, CAST(@StringVal AS FLOAT)

SET @StringVal = '123456.1234'
SELECT @StringVal, CAST(@StringVal AS FLOAT)
于 2012-06-18T19:44:50.687 回答
4

你的主要问题不是小数点右边的东西,而是左边的东西。类型声明中的两个值是精度和比例。

来自 MSDN:“精度是数字中的位数。比例是数字中小数点右侧的位数。例如,数字 123.45 的精度为 5,小数位数为 2。”

如果您指定 (10, 4),则意味着您只能在小数点左侧存储 6 位数字,或者最大数字为 999999.9999。任何比这更大的都会导致溢出。

于 2012-06-18T21:53:34.660 回答
4

在将它们放入该列之前,您必须自己将值截断为字符串。

否则,如果您想要更多小数位,则需要更改小数列的声明。

于 2012-06-18T18:52:39.527 回答
3

使用自定义函数实现。这将检查字符串值是否可以安全地转换为十进制

CREATE FUNCTION [dbo].[TryParseAsDecimal]
(
    @Value      NVARCHAR(4000)
    ,@Precision INT
    ,@Scale     INT
)

RETURNS BIT
AS
BEGIN

    IF(ISNUMERIC(@Value) =0) BEGIN
        RETURN CAST(0 AS BIT)
    END
    SELECT @Value = REPLACE(@Value,',','') --Removes the comma

    --This function validates only the first part eg '1234567.8901111111'
    --It validates only the values before the '.' ie '1234567.'
    DECLARE @Index          INT
    DECLARE @Part1Length    INT 
    DECLARE @Part1          VARCHAR(4000)   

    SELECT @Index = CHARINDEX('.', @Value, 0)
    IF (@Index>0) BEGIN
        --If decimal places, extract the left part only and cast it to avoid leading zeros (eg.'0000000001' => '1')
        SELECT @Part1 =LEFT(@Value, @Index-1);
        SELECT @Part1=SUBSTRING(@Part1, PATINDEX('%[^0]%', @Part1+'.'), LEN(@Part1));
        SELECT @Part1Length = LEN(@Part1);
    END
    ELSE BEGIN
        SELECT @Part1 =CAST(@Value AS DECIMAL);
        SELECT @Part1Length= LEN(@Part1)
    END 

    IF (@Part1Length > (@Precision-@Scale)) BEGIN
        RETURN CAST(0 AS BIT)
    END

    RETURN CAST(1 AS BIT)

END
于 2012-08-02T09:23:39.393 回答
2

我知道这是一个老问题,但比尔似乎是唯一一个真正“解释”了这个问题的人。其他人似乎都在为滥用声明提出复杂的解决方案。

“类型声明中的两个值是精度和比例。”

...

“如果您指定 (10, 4),这意味着您只能在小数点左侧存储 6 位数字,或者最大数字为 999999.9999。任何大于该数字的内容都会导致溢出。”

因此,如果您声明DECIMAL(10,4)总共可以有10 个数字,其中4个在小数点之后。所以 123456.1234 有 10 位,小数点后 4 位。这将适合DECIMAL(10,4). 1234567.1234 会抛出错误。有 11 位数字适合 10 位数字空间,小数点后必须使用 4 位数字。修剪小数点左侧的数字不是一种选择。如果您的 11 个字符是 123456.12345,这不会引发错误,因为从十进制值末尾进行修整(舍入)是可以接受的。

声明小数时,请始终尝试声明您的列将实际使用的最大值以及您希望看到的最大小数位数。因此,如果您的列只显示最大值为 100 万的值,并且您只关心前两位小数,请声明为DECIMAL(9,2). 在引发错误之前,这将为您提供最大数量的 9,999,999.99。

在尝试修复之前了解问题,将确保您选择适合您的情况的修复,并帮助您了解需要/有效修复的原因。

再说一次,我知道我迟到了五年。但是,我为此提供了两分钱的解决方案,(根据您的评论判断该列已设置为DECIMAL(10,4)并且无法更改)最简单的方法是两个步骤。检查你的小数点距离不超过 10 点,然后修剪到 10 位数。

CASE WHEN CHARINDEX('.',CONVERT(VARCHAR(50),[columnName]))>10 THEN 'DealWithIt'
ELSE LEFT(CONVERT(VARCHAR(50),[columnName]),10) 
END AS [10PointDecimalString]

我将其保留为字符串的原因是您可以处理小数点左侧长度超过 10 位的值。

但它是一个开始。

于 2017-02-16T13:57:13.963 回答
2
create function [Sistema].[fParseDecimal]
(
    @Valor nvarchar(4000)
)
returns decimal(18, 4) as begin
    declare @Valores table (Valor varchar(50));

    insert into @Valores values (@Valor);

    declare @Resultado decimal(18, 4) = (select top 1 
        cast('' as xml).value('sql:column("Valor") cast as xs:decimal ?', 'decimal(18, 4)')
    from @Valores);
    
    return @Resultado;
END
于 2020-11-26T21:40:01.607 回答
1

如果您需要 ROUND 结果,而不是截断,可以使用这个:

select convert(decimal(38,4), round(convert(decimal(38,10), '123456789.1234567'),4))

这将返回以下内容:

'123456789.1235' for '123456789.1234567'
'123456789.1234' for '123456789.1234467'
于 2018-12-12T07:48:00.087 回答
1

在 MySQL 中

select convert( if( listPrice REGEXP '^[0-9]+$', listPrice, '0' ), DECIMAL(15, 3) ) from MyProduct WHERE 1
于 2020-11-18T22:14:53.813 回答