web-dev-qa-db-ja.com

CASTおよびIsNumeric

次のクエリが「データ型varcharからbigintへの変換エラー」を返すのはなぜですか? IsNumericはCASTを安全にしませんか?キャスト内のすべての数値データ型を試してみましたが、同じ「エラー変換...」エラーが発生しました。オーバーフローは別のエラーであるため、結果の数値のサイズが問題になるとは思わない。

興味深いのは、管理スタジオでは、エラーが戻る前に、結果が実際に結果ペインに一瞬表示されることです。

SELECT CAST(myVarcharColumn AS bigint)  
FROM myTable  
WHERE IsNumeric(myVarcharColumn) = 1 AND myVarcharColumn IS NOT NULL  
GROUP BY myVarcharColumn

何かご意見は?

26
Mark Bostleman

Varchar値を任意の数値型に変換できる場合、IsNumericは1を返します。これには、int、bigint、decimal、numeric、real&floatが含まれます。

科学的表記法が問題を引き起こしている可能性があります。例えば:

Declare @Temp Table(Data VarChar(20))

Insert Into @Temp Values(NULL)
Insert Into @Temp Values('1')
Insert Into @Temp Values('1e4')
Insert Into @Temp Values('Not a number')

Select Cast(Data as bigint)
From   @Temp
Where  IsNumeric(Data) = 1 And Data Is Not NULL

IsNumericで使用できるトリックがあり、科学表記法で数値に対して0を返します。同様のトリックを適用して、小数値を防ぐことができます。

IsNumeric(YourColumn + 'e0')

IsNumeric(YourColumn + '.0e0')

やってみよう。

SELECT CAST(myVarcharColumn AS bigint)
FROM myTable
WHERE IsNumeric(myVarcharColumn + '.0e0') = 1 AND myVarcharColumn IS NOT NULL
GROUP BY myVarcharColumn
57
G Mastros

バックグラウンド:

他のサードパーティベンダーから新しいデータを常に受信するサードパーティデータベースを使用しています。
結果を保存するために使用される恐ろしいvarcharフィールドを解析するのが私の仕事です。
できるだけ多くのデータを解析したいのですが、このソリューションでは、有効なエントリが見落とされないようにデータを「クリーンアップ」する方法を示します。

  1. 一部の結果はフリーテキストです。
  2. いくつかは列挙です(はい、いいえ、青、黒など)。
  3. いくつかは整数です。
  4. その他は小数を使用します。
  5. 多くはパーセンテージであり、整数に変換すると後でトリップする可能性があります。

特定の10進数の範囲(該当する場合は-1.4から3.6など)を照会する必要がある場合、オプションは制限されます。
@ GMastrosの提案を使用して 'e0'を追加するように、以下のクエリを更新しました。
@ GMastrosに感謝します。これにより、余分な2行のロジックを節約できました。

解決:

--NOTE: I'd recommend you use this to convert your numbers and store them in a separate table (or field).
--      This way you may reuse them when when working with legacy/3rd-party systems, instead of running these calculations on the fly each time.
SELECT Result.Type, Result.Value, Parsed.CleanValue, Converted.Number[Number - Decimal(38,4)],
       (CASE WHEN Result.Value IN ('0', '1', 'True', 'False') THEN CAST(Result.Value as Bit) ELSE NULL END)[Bit],--Cannot convert 1.0 to Bit, it must be in Integer format already.
       (CASE WHEN Converted.Number BETWEEN 0 AND 255 THEN CAST(Converted.Number as TinyInt) ELSE NULL END)[TinyInt],
       (CASE WHEN Converted.Number BETWEEN -32768 AND 32767 AND Result.Value LIKE '%\%%' ESCAPE '\' THEN CAST(Converted.Number / 100.0 as Decimal(9,4)) ELSE NULL END)[Percent],
       (CASE WHEN Converted.Number BETWEEN -32768 AND 32767 THEN CAST(Converted.Number as SmallInt) ELSE NULL END)[SmallInt],
       (CASE WHEN Converted.Number BETWEEN -214748.3648 AND 214748.3647 THEN CAST(Converted.Number as SmallMoney) ELSE NULL END)[SmallMoney],
       (CASE WHEN Converted.Number BETWEEN -2147483648 AND 2147483647 THEN CAST(Converted.Number as Int) ELSE NULL END)[Int],
       (CASE WHEN Converted.Number BETWEEN -2147483648 AND 2147483647 THEN CAST(CAST(Converted.Number as Decimal(10)) as Int) ELSE NULL END)[RoundInt],--Round Up or Down instead of Truncate.
       (CASE WHEN Converted.Number BETWEEN -922337203685477.5808 AND 922337203685477.5807 THEN CAST(Converted.Number as Money) ELSE NULL END)[Money],
       (CASE WHEN Converted.Number BETWEEN -9223372036854775808 AND 9223372036854775807 THEN CAST(Converted.Number as BigInt) ELSE NULL END)[BigInt],
       (CASE WHEN Parsed.CleanValue IN ('1', 'True', 'Yes', 'Y', 'Positive', 'Normal')   THEN CAST(1 as Bit)
             WHEN Parsed.CleanValue IN ('0', 'False', 'No', 'N', 'Negative', 'Abnormal') THEN CAST(0 as Bit) ELSE NULL END)[Enum],
       --I couln't use just Parsed.CleanValue LIKE '%e%' here because that would match on "True" and "Negative", so I also had to match on only allowable characters. - 02/13/2014 - MCR.
       (CASE WHEN ISNUMERIC(Parsed.CleanValue) = 1 AND Parsed.CleanValue LIKE '%e%' THEN Parsed.CleanValue ELSE NULL END)[Exponent]
  FROM
  (
    VALUES ('Null', NULL), ('EmptyString', ''), ('Spaces', ' - 2 . 8 % '),--Tabs and spaces mess up IsNumeric().
           ('Bit', '0'), ('TinyInt', '123'), ('Int', '123456789'), ('BigInt', '1234567890123456'),
           --('VeryLong', '12345678901234567890.1234567890'),
           ('VeryBig', '-1234567890123456789012345678901234.5678'),
           ('TooBig',  '-12345678901234567890123456789012345678.'),--34 (38-4) is the Longest length of an Integer supported by this query.
           ('VeryLong', '-1.2345678901234567890123456789012345678'),
           ('TooLong', '-12345678901234567890.1234567890123456789'),--38 Digits is the Longest length of a Number supported by the Decimal data type.
           ('VeryLong', '000000000000000000000000000000000000001.0000000000000000000000000000000000000'),--Works because Casting ignores leading zeroes.
           ('TooLong', '.000000000000000000000000000000000000000'),--Exceeds the 38 Digit limit for all Decimal types after the decimal-point.
           --Dot(.), Plus(+), Minus(-), Comma(,), DollarSign($), BackSlash(\), Tab(0x09), and Letter-E(e) all yeild false-posotives with IsNumeric().
           ('Decimal', '.'), ('Decimal', '.0'), ('Decimal', '3.99'),
           ('Positive', '+'), ('Positive', '+20'),
           ('Negative', '-'), ('Negative', '-45'), ('Negative', '- 1.23'),
           ('Comma', ','), ('Comma', '1,000'),
           ('Money', '$'), ('Money', '$10'),
           ('Percent', '%'), ('Percent', '110%'),--IsNumeric will kick out Percent(%) signs.
           ('BkSlash', '\'), ('Tab', CHAR(0x09)),--I've actually seen tab characters in our data.
           ('Exponent', 'e0'), ('Exponent', '100e-999'),--No SQL-Server datatype could hold this number, though it is real.
           ('Enum', 'True'), ('Enum', 'Negative')
  ) AS Result(Type, Value)--O is for Observation.
  CROSS APPLY
  ( --This Step is Optional.  If you have Very Long numbers with tons of leading zeros, then this is useful.  Otherwise is overkill if all the numbers you want have 38 or less digits.
    --Casting of trailing zeros count towards the max 38 digits Decimal can handle, yet Cast ignores leading-zeros.  This also cleans up leading/trailing spaces. - 02/25/2014 - MCR.
    SELECT LTRIM(RTRIM(SUBSTRING(Result.Value, PATINDEX('%[^0]%', Result.Value + '.'), LEN(Result.Value))))[Value]
  ) AS Trimmed
  CROSS APPLY
  (
    SELECT --You will need to filter out other Non-Keyboard ASCII characters (before Space(0x20) and after Lower-Case-z(0x7A)) if you still want them to be Cast as Numbers. - 02/15/2014 - MCR.
           REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(Trimmed.Value,--LTRIM(RTRIM(Result.Value)),
           (CHAR(0x0D) + CHAR(0x0A)), ''),--Believe it or not, we have people that press carriage return after entering in the value.
           CHAR(0x09), ''),--Apparently, as people tab through controls on a page, some of them inadvertently entered Tab's for values.
           ' ', ''),--By replacing spaces for values (like '- 2' to work), you open the door to values like '00 12 3' - your choice.
           '$', ''), ',', ''), '+', ''), '%', ''), '/', '')[CleanValue]
  ) AS Parsed--P is for Parsed.
  CROSS APPLY
  ( --NOTE: I do not like my Cross-Applies to feed into each other.
    --      I'm paranoid it might affect performance, but you may move this into the select above if you like. - 02/13/2014 - MCR.
    SELECT (CASE WHEN ISNUMERIC(Parsed.CleanValue + 'e0') = 1--By concatenating 'e0', I do not need to check for: Parsed.CleanValue NOT LIKE '%e%' AND Parsed.CleanValue NOT IN ('.', '-')
                 --  If you never plan to work with big numbers, then could use Decimal(19,4) would be best as it only uses 9 storage bytes compared to the 17 bytes that 38 precision requires.
                 --  This might help with performance, especially when converting a lot of data.
                  AND CHARINDEX('.', REPLACE(Parsed.CleanValue, '-', '')) - 1    <= (38-4)--This is the Longest Integer supported by Decimal(38,4)).
                  AND LEN(REPLACE(REPLACE(Parsed.CleanValue, '-', ''), '.', '')) <= 38--When casting to a Decimal (of any Precision) you cannot exceed 38 Digits. - 02/13/2014 - MCR.
                 THEN CAST(Parsed.CleanValue as Decimal(38,4))--Scale of 4 used is the max that Money has.  This is the biggest number SQL Server can hold.
                 ELSE NULL END)[Number]
  ) AS Converted--C is for Converted.

出力:

以下のスクリーンショットはフォーマットされており、StackOverflowに合うように切り詰められています。
実際の結果にはさらに列があります。 MikeTeeVee's IsNumeric Casting

研究:

各クエリの横に結果があります。
IsNumericの欠点とCASTingの制限を見ることは興味深いです。
これを示しているので、上記のクエリを作成するために行った背景調査を見ることができます。
各設計の決定を理解することが重要です(何かをカットすることを考えている場合)。

SELECT ISNUMERIC('')--0.  This is understandable, but your logic may want to default these to zero.
SELECT ISNUMERIC(' ')--0.  This is understandable, but your logic may want to default these to zero.
SELECT ISNUMERIC('%')--0.
SELECT ISNUMERIC('1%')--0.
SELECT ISNUMERIC('e')--0.
SELECT ISNUMERIC('  ')--1.  --Tab.
SELECT ISNUMERIC(CHAR(0x09))--1.  --Tab.
SELECT ISNUMERIC(',')--1.
SELECT ISNUMERIC('.')--1.
SELECT ISNUMERIC('-')--1.
SELECT ISNUMERIC('+')--1.
SELECT ISNUMERIC('$')--1.
SELECT ISNUMERIC('\')--1.  '
SELECT ISNUMERIC('e0')--1.
SELECT ISNUMERIC('100e-999')--1.  No SQL-Server datatype could hold this number, though it is real.
SELECT ISNUMERIC('3000000000')--1.  This is bigger than what an Int could hold, so code for these too.
SELECT ISNUMERIC('1234567890123456789012345678901234567890')--1.  Note: This is larger than what the biggest Decimal(38) can hold.
SELECT ISNUMERIC('- 1')--1.
SELECT ISNUMERIC('  1  ')--1.
SELECT ISNUMERIC('True')--0.
SELECT ISNUMERIC('1/2')--0.  No love for fractions.

SELECT CAST('e0'  as Int)--0.  Surpise!  Casting to Decimal errors, but for Int is gives us zero, which is wrong.
SELECT CAST('0e0'  as Int)--0.  Surpise!  Casting to Decimal errors, but for Int is gives us zero, which is wrong.
SELECT CAST(CHAR(0x09) as Decimal(12,2))--Error converting data type varchar to numeric.  --Tab.
SELECT CAST('   1' as Decimal(12,2))--Error converting data type varchar to numeric.  --Tab.
SELECT CAST(REPLACE('   1', CHAR(0x09), '') as Decimal(12,2))--Error converting data type varchar to numeric.  --Tab.
SELECT CAST(''  as Decimal(12,2))--Error converting data type varchar to numeric.
SELECT CAST(''  as Int)--0.  Surpise!  Casting to Decimal errors, but for Int is gives us zero, which is wrong.
SELECT CAST(',' as Decimal(12,2))--Error converting data type varchar to numeric.
SELECT CAST('.' as Decimal(12,2))--Error converting data type varchar to numeric.
SELECT CAST('-' as Decimal(12,2))--Arithmetic overflow error converting varchar to data type numeric.
SELECT CAST('+' as Decimal(12,2))--Arithmetic overflow error converting varchar to data type numeric.
SELECT CAST('$' as Decimal(12,2))--Error converting data type varchar to numeric.
SELECT CAST('$1' as Decimal(12,2))--Error converting data type varchar to numeric.
SELECT CAST('1,000' as Decimal(12,2))--Error converting data type varchar to numeric.
SELECT CAST('- 1'   as Decimal(12,2))--Error converting data type varchar to numeric.  (Due to spaces).
SELECT CAST('  1  ' as Decimal(12,2))--1.00  Leading and trailing spaces are okay.
SELECT CAST('1.' as Decimal(12,2))--1.00
SELECT CAST('.1' as Decimal(12,2))--0.10
SELECT CAST('-1' as Decimal(12,2))--1.00
SELECT CAST('+1' as Decimal(12,2))--1.00
SELECT CAST('True'  as Bit)--1
SELECT CAST('False' as Bit)--0
--Proof: The Casting to Decimal cannot exceed 38 Digits, even if the precision is well below 38.
SELECT CAST('1234.5678901234567890123456789012345678' as Decimal(8,4))--1234.5679
SELECT CAST('1234.56789012345678901234567890123456789' as Decimal(8,4))--Arithmetic overflow error converting varchar to data type numeric.

--Proof: Casting of trailing zeros count towards the max 38 digits Decimal can handle, yet it ignores leading-zeros.
SELECT CAST('.00000000000000000000000000000000000000' as Decimal(8,4))--0.0000  --38 Digits after the decimal point.
SELECT CAST('000.00000000000000000000000000000000000000' as Decimal(8,4))--0.0000  --38 Digits after the decimal point and 3 zeros before the decimal point.
SELECT CAST('.000000000000000000000000000000000000000' as Decimal(8,4))--Arithmetic overflow error converting varchar to data type numeric.  --39 Digits after the decimal point.
SELECT CAST('1.00000000000000000000000000000000000000' as Decimal(8,4))--Arithmetic overflow error converting varchar to data type numeric.  --38 Digits after the decimal point and 1 non-zero before the decimal point.
SELECT CAST('000000000000000000000000000000000000001.0000000000000000000000000000000000000' as Decimal(8,4))--1.0000

--Caveats: When casting to an Integer:
SELECT CAST('3.0' as Int)--Conversion failed when converting the varchar value '3.0' to data type int.
--NOTE: When converting from character data to Int, you may want to do a double-conversion like so (if you want to Round your results first):
SELECT CAST(CAST('3.5'  as Decimal(10))   as Int)--4.  Decimal(10) has no decimal precision, so it rounds it to 4 for us BEFORE converting to an Int.
SELECT CAST(CAST('3.5'  as Decimal(11,1)) as Int)--3.  Decimal (11,1) HAS decimal precision, so it stays 3.5 before converting to an Int, which then truncates it.
--These are the best ways to go if you simply want to Truncate or Round.
SELECT CAST(CAST('3.99' as Decimal(10)) as Int)--3.  Good Example of Rounding.
SELECT CAST(FLOOR('3.99') as Int)--3.  Good Example fo Truncating.
7
MikeTeeVee

最良の解決策は、varchar列への整数の格納を停止することです。明らかに、データは数値として解釈可能であるが、そのようにキャストできないというデータの問題があります。問題のあるレコードを見つけて、修正可能なデータである場合は修正する必要があります。保存する内容と、それが最初にvarcharである理由に応じて、データではなくクエリを修正する必要があります。ただし、現在のクエリを爆破しているレコードを最初に見つけた場合も、簡単に実行できます。

それを行う方法が問題です。 charindexを使用して、データ内の小数部を検索して小数部(変換する.0以外)があるかどうかを確認するのは比較的簡単です。また、eまたは$を含むレコード、または既に提供されているソースに応じて数値として挿入できる他の文字を検索することもできます。多くのレコードがない場合、特にそのフィールドで最初に並べ替える場合は特に、データのクイックビジュアルスキャンでデータが見つかる可能性があります。

時々、クエリを爆破している不良データを見つけるのにこだわっているとき、データを一時テーブルに入れて、爆発するものが見つかるまでバッチで処理を試みました(補間を使用)。最初の1000から開始します(適切なレコードを削除する場合、order byを使用することを忘れないでください。数百万のレコードがより大きな数で始まる場合、1000は最良の推測にすぎません)。合格した場合、それらの1000レコードを削除し、次のバッチを選択します。失敗したら、より小さいバッチを選択します。視覚的に簡単にスキャンできる数に到達すると、問題が見つかります。何百万ものレコードと、試したクエリ(基本的には何が間違っているのかを推測する)のどれもが問題を見つけられなかったという奇妙なエラーがある場合、問題のレコードをかなり迅速に見つけることができました。

4
HLGEM

これを試して、まだエラーが発生するかどうかを確認してください...

SELECT CAST(CASE 
            WHEN IsNumeric(myVarcharColumn) = 0
                THEN 0
            ELSE myVarcharColumn
            END AS BIGINT)
FROM myTable
WHERE IsNumeric(myVarcharColumn) = 1
    AND myVarcharColumn IS NOT NULL
GROUP BY myVarcharColumn
3
Kevin Fairchild

ISNUMERICはただ...バカです。あなたはまったくそれを使うだろう。以下のすべてのケースは、1を返します。

_ISNUMERIC('-')
ISNUMERIC('.')
ISNUMERIC('-$.') 
_

代わりに使用する整数型の場合:ISNUMERIC(@Value) = 1を使用するだけで、_(@Value NOT LIKE '[^0-9]') OR (@Value NOT LIKE '-[^0-9]'_

唯一の良い解決策は、ISNUMERICを使用しないことです。

2
Arkady

[〜#〜] bol [〜#〜] によれば、ISNUMERICは、入力式が有効な数値データ型に評価されると1を返します。それ以外の場合は0を返します。

有効な数値データ型には次のものがあります。

  • int
  • 数値
  • ビッグ
  • お金
  • 小さい
  • 小銭
  • tinyint
  • float
  • 小数
  • リアル

したがって、他の人が指摘したように、[〜#〜] isnumeric [〜#〜]テストに合格するが、bigintへのキャストに失敗するデータがあります。

1
kristof

ケースに入れてみてください:

select CASE WHEN IsNumeric(mycolumn) = 1 THEN CAST(mycolumn as bigint) END
FROM stack_table
WHERE IsNumeric(mycolumn) = 1
GROUP BY mycolumn
1

私は同じ問題を抱えていましたが、2008 SQLのImとしてスカラー関数を思いつきました

ALTER Function [dbo].[IsInteger](@Value VarChar(18))
Returns Bit
As 
Begin

  Return IsNull(
     (Select Case When CharIndex('.', @Value) > 0 
                  Then 0
                  Else 1
             End
      Where IsNumeric(@Value + 'e0') = 1), 0)    
End

2012年の場合は、TRY_CONVERT

1

MSSQL 2014でも、フルストップではなくコンマによってトリガーされる同じ問題がありました。isnumeric( '9090,23')は1を返します。 cast( '9090,23' as float)は失敗します

「、」を「。」に置き換えました

0
user5480949

この状況で役立つDAX関数(IsErrorまたはIfError)がありますが、SQL Server 2008 R2にはありません。 SQL Server用の追加の分析パッケージのように見えます。

0
Greg