web-dev-qa-db-fra.com

CAST et IsNumeric

Pourquoi la requête suivante renverrait-elle "Erreur lors de la conversion du type de données varchar en bigint"? IsNumeric ne rend-il pas le CAST sûr? J'ai essayé tous les types de données numériques de la distribution et j'obtiens la même erreur "Erreur de conversion ...". Je ne crois pas que la taille du nombre résultant soit un problème car le débordement est une erreur différente.

La chose intéressante est que, dans le studio de gestion, les résultats s'affichent réellement dans le volet des résultats pendant une fraction de seconde avant que l'erreur ne se reproduise.

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

Des pensées?

26
Mark Bostleman

IsNumeric renvoie 1 si la valeur varchar peut être convertie en N'IMPORTE QUEL type de nombre. Cela inclut int, bigint, décimal, numérique, réel et flottant.

La notation scientifique pourrait vous poser un problème. Par exemple:

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

Il existe une astuce que vous pouvez utiliser avec IsNumeric pour qu'il renvoie 0 pour les nombres avec notation scientifique. Vous pouvez appliquer une astuce similaire pour éviter les valeurs décimales.

IsNumeric (YourColumn + 'e0')

IsNumeric (YourColumn + '.0e0')

Essaye le.

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

Contexte:

J'utilise une base de données tierce qui reçoit constamment de nouvelles données d'autres fournisseurs tiers.
C'est mon travail d'analyser un horrible champ varchar utilisé pour stocker les résultats.
Nous voulons analyser autant de données que possible, et cette solution vous montre comment vous pouvez "nettoyer" les données afin que les entrées valides ne soient pas ignorées.

  1. Certains résultats sont en texte libre.
  2. Certains sont des énumérations (oui, non, bleu, noir, etc.).
  3. Certains sont des entiers.
  4. D'autres utilisent des décimales.
  5. Beaucoup sont des pourcentages qui, s'ils étaient convertis en nombre entier, pourraient vous faire trébucher plus tard.

Si je dois rechercher une plage décimale donnée (disons -1,4 à 3,6 le cas échéant), mes options sont limitées.
J'ai mis à jour ma requête ci-dessous pour utiliser la suggestion @GMastros pour ajouter "e0".
Merci @GMastros, cela m'a fait économiser 2 lignes de logique supplémentaires.

Solution:

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

Production:

La capture d'écran ci-dessous a été formatée et coupée pour s'adapter à StackOverflow.
Les résultats réels ont plus de colonnes. MikeTeeVee's IsNumeric Casting

Recherche:

À côté de chaque requête se trouve le résultat.
Il est intéressant de voir les lacunes d'IsNumeric ainsi que les limites de CASTing.
Je le montre afin que vous puissiez voir les recherches de base qui ont servi à écrire la requête ci-dessus.
Il est important de comprendre chaque décision de conception (au cas où vous envisagez de couper quoi que ce soit).

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

La meilleure solution serait d'arrêter de stocker des entiers dans une colonne varchar. De toute évidence, il existe un problème de données dans lequel les données sont interprétables sous forme numérique mais ne peuvent pas être converties en tant que telles. Vous devez trouver les enregistrements qui sont le problème et les corriger si les données sont telles qu'elles peuvent et doivent être corrigées. Selon ce que vous stockez et pourquoi c'est un varchar pour commencer, vous devrez peut-être corriger la requête au lieu des données. Mais cela sera plus facile à faire également si vous trouvez d'abord les enregistrements qui font exploser votre requête actuelle.

Comment faire est le problème. Il est relativement facile de rechercher une décimale dans les données pour voir si vous avez des décimales (autres que 0 qui se convertiraient) en utilisant charindex. Vous pouvez également rechercher tout enregistrement contenant e ou $ ou tout autre caractère pouvant être interprété comme numérique en fonction des sources déjà fournies. Si vous n'avez pas beaucoup d'enregistrements, une analyse visuelle rapide des données les trouvera probablement, surtout si vous triez d'abord sur ce champ.

Parfois, lorsque je suis resté coincé à trouver les mauvaises données qui font exploser une requête, j'ai placé les données dans une table temporaire, puis j'ai essayé de les traiter par lots (en utilisant l'interpolation) jusqu'à ce que je trouve celle sur laquelle elle explose. Commencez par les 1000 premiers (n'oubliez pas d'utiliser la commande par ou vous n'obtiendrez pas les mêmes résultats lorsque vous supprimez les bons enregistrements et 1000 n'est une meilleure estimation que si vous avez des millions d'enregistrements commençant par un plus grand nombre). S'il réussit, supprimez ces 1000 enregistrements et sélectionnez le lot suivant. En cas d'échec, sélectionnez un lot plus petit. Une fois que vous avez atteint un nombre qui peut facilement être scanné visuellement, vous trouverez le problème. J'ai été en mesure de trouver des enregistrements de problèmes assez rapidement lorsque j'ai des millions d'enregistrements et une erreur étrange qu'aucune des requêtes que j'ai essayées (qui sont essentiellement des suppositions sur ce qui pourrait être faux) n'a trouvé le problème.

4
HLGEM

Essayez ceci et voyez si vous obtenez toujours une erreur ...

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 est juste ... stupide. Vous devriez l'utiliser du tout. Tous les cas ci-dessous retournent 1:

ISNUMERIC('-')
ISNUMERIC('.')
ISNUMERIC('-$.') 

Pour tout type entier, utilisez plutôt: ISNUMERIC(@Value) = 1 utilisez simplement: (@Value NOT LIKE '[^0-9]') OR (@Value NOT LIKE '-[^0-9]'

La seule bonne solution est de ne pas utiliser ISNUMERIC.

2
Arkady

Selon BOL ISNUMERIC renvoie 1 lorsque l'expression d'entrée est évaluée en un type de données numérique valide; sinon, elle renvoie 0.

Les types de données numériques valides sont les suivants:

  • int
  • numérique
  • bigint
  • argent
  • petite
  • petit argent
  • tinyint
  • float
  • décimal
  • réel

Ainsi, comme d'autres l'ont souligné, vous aurez des données qui passeront [~ # ~] le test isnumeric [~ # ~] mais échouera lors de la conversion en bigint

1
kristof

Essayez de l'envelopper dans un étui:

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

J'ai eu le même problème et j'ai trouvé la fonction scalaire comme Im sur 2008 SQL

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

Si vous êtes en 2012, vous pouvez utiliser TRY_CONVERT

1

J'ai eu le même problème dans MSSQL 2014 déclenché par une virgule au lieu du point final: isnumeric ('9090,23') donne 1; cast ('9090,23' comme float) échoue

J'ai remplacé "," par "."

0
user5480949

il existe des fonctions DAX (IsError ou IfError) qui pourraient aider dans cette situation, mais nous n'en avons pas sur notre SQL Server 2008 R2. Ressemble à un package d'analyse supplémentaire pour SQL Server.

0
Greg