【问题标题】:VARCHAR to DECIMALVARCHAR 转 DECIMAL
【发布时间】:2012-06-18 18:48:41
【问题描述】:

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

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

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

【问题讨论】:

  • 你如何期望123456789.1234567 适合DECIMAL(10,4)
  • 它必须是十进制还是您可以使用浮点数或实数数据类型:msdn.microsoft.com/en-us/library/ms173773.aspx
  • 如果它不合适,那么我想确定它。只需要像 sql 2012 中的 try_parse 这样的方式,但不幸的是 sql server 版本是 2008
  • 你对其他值有什么问题?
  • 出现算术溢出错误

标签: sql sql-server


【解决方案1】:

测试后发现问题不是小数位,而是精度(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 个整数 + 4 个浮点数 = 13 个字符

【讨论】:

    【解决方案2】:

    我的解释在代码中。 :)

    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)))
    

    【讨论】:

      【解决方案3】:

      我想出了以下解决方案:

      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)NULL)。

      它被硬编码为decimal(38,10)类型,所以如果你需要不同的精度,请更改代码中的常量(38、10、29)。

      它是如何工作的?结果是

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

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

      以下是几个转化示例:

      【讨论】:

        【解决方案4】:

        您还没有解释为什么不能使用 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)
        

        【讨论】:

        • 我不能使用 float 或任何其他类型,因为目标列是 Decimal(10,4),我无法修改。
        【解决方案5】:

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

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

        SELECT CONVERT(decimal(11,2), REPLACE('6.999,50', '.', ''))

        将产生 6999,50 作为小数

        【讨论】:

          【解决方案6】:

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

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

          【讨论】:

            【解决方案7】:

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

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

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

            【讨论】:

              【解决方案8】:

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

              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
              

              【讨论】:

              • 你是救生员!非常感谢你。 :)
              【解决方案9】:

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

              “类型声明中的两个值是精度和小数位数。”

              ...

              "如果你指定 (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。

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

              再一次,我知道我迟到了五年。 但是,我为此提供了两分钱的解决方案,(根据您的 cmets 判断,该列已设置为 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 位的值。

              但这只是一个开始。

              【讨论】:

                【解决方案10】:
                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
                

                【讨论】:

                  【解决方案11】:

                  如果你需要对结果进行 ROUND,而不是截断,可以使用这个:

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

                  这将返回以下内容:

                  '123456789.1235' for '123456789.1234567'
                  '123456789.1234' for '123456789.1234467'
                  

                  【讨论】:

                    【解决方案12】:

                    在 MySQL 中

                    select convert( if( listPrice REGEXP '^[0-9]+$', listPrice, '0' ), DECIMAL(15, 3) ) from MyProduct WHERE 1
                    

                    【讨论】:

                      猜你喜欢
                      • 2019-01-22
                      • 2014-04-12
                      • 2019-02-25
                      • 1970-01-01
                      • 2021-11-04
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      相关资源
                      最近更新 更多