Comment convertir une chaîne en entier et avoir 0 en cas d'erreur dans le cast avec PostgreSQL?

128

Dans PostgreSQL, j'ai une table avec une colonne varchar. Les données sont censées être des nombres entiers et j'en ai besoin en type entier dans une requête. Certaines valeurs sont des chaînes vides. Le suivant:

SELECT myfield::integer FROM mytable

rendements ERROR: invalid input syntax for integer: ""

Comment puis-je interroger un casting et avoir 0 en cas d'erreur lors du casting dans postgres?

silviot
la source

Réponses:

161

J'étais moi-même aux prises avec un problème similaire, mais je ne voulais pas les frais généraux d'une fonction. J'ai proposé la requête suivante:

SELECT myfield::integer FROM mytable WHERE myfield ~ E'^\\d+$';

Postgres raccourcit ses conditions, donc vous ne devriez pas obtenir de non-entiers qui atteignent votre :: integer cast. Il gère également les valeurs NULL (elles ne correspondent pas à l'expression rationnelle).

Si vous voulez des zéros au lieu de ne pas sélectionner, une instruction CASE devrait fonctionner:

SELECT CASE WHEN myfield~E'^\\d+$' THEN myfield::integer ELSE 0 END FROM mytable;
Anthony Briggs
la source
14
Je recommanderais fortement de suivre la suggestion de Matthew. Cette solution a des problèmes avec les chaînes qui ressemblent à des nombres mais qui sont plus grandes que la valeur maximale que vous pouvez placer dans un entier.
pilif
4
Je seconde le commentaire de Pilif. cette valeur maximale est un bogue qui attend de se produire. le but de ne pas lancer d'erreur est de ne pas lancer d'erreur lorsque les données ne sont pas valides. cette réponse acceptée ne résout PAS cela. merci Matthew! bon travail!
Shawn Kovac
3
Aussi formidable que soit la réponse de Matthew, j'avais juste besoin d'un moyen rapide et sale de gérer pour vérifier certaines données. J'admets également que mes propres connaissances font actuellement défaut pour définir les fonctions en SQL. Je n'étais intéressé que par les nombres entre 1 et 5 chiffres, j'ai donc changé l'expression régulière en E'\\d{1,5}$'.
Bobort
3
Oui, oui cette solution est relativement rapide et sale, mais dans mon cas je savais quelles données j'avais et que le tableau était relativement court. C'est beaucoup plus facile que d'écrire (et de déboguer) une fonction entière. La {1,5}limite de @ Bobort ci-dessus sur les chiffres est peut-être une bonne idée si vous êtes préoccupé par le dépassement de capacité, mais cela masquera des nombres plus grands, ce qui pourrait causer des problèmes si vous convertissez une table. Personnellement, je préfère avoir l'erreur de requête à l'avant et savoir que certains de mes "entiers" sont vicieux (vous pouvez également sélectionner avec le E'\\d{6,}$'premier pour vous en assurer).
Anthony Briggs
1
@Anthony Briggs: Cela ne fonctionnera pas si mon champ contient "'" ou "," ou ".", Ou "-"
Stefan Steiger
100

Vous pouvez également créer votre propre fonction de conversion, dans laquelle vous pouvez utiliser des blocs d'exception:

CREATE OR REPLACE FUNCTION convert_to_integer(v_input text)
RETURNS INTEGER AS $$
DECLARE v_int_value INTEGER DEFAULT NULL;
BEGIN
    BEGIN
        v_int_value := v_input::INTEGER;
    EXCEPTION WHEN OTHERS THEN
        RAISE NOTICE 'Invalid integer value: "%".  Returning NULL.', v_input;
        RETURN NULL;
    END;
RETURN v_int_value;
END;
$$ LANGUAGE plpgsql;

Essai:

=# select convert_to_integer('1234');
 convert_to_integer 
--------------------
               1234
(1 row)

=# select convert_to_integer('');
NOTICE:  Invalid integer value: "".  Returning NULL.
 convert_to_integer 
--------------------

(1 row)

=# select convert_to_integer('chicken');
NOTICE:  Invalid integer value: "chicken".  Returning NULL.
 convert_to_integer 
--------------------

(1 row)
Matthew Wood
la source
8
contrairement à la réponse acceptée, cette solution ici est plus correcte car elle peut également traiter des nombres trop gros pour tenir dans un entier et elle est également susceptible d'être plus rapide car elle ne fonctionne pas de validation dans le cas courant (= chaînes valides )
pilif
Comment qualifieriez - sur les champs spécifiques que vous lancez chaîne en entier en utilisant votre fonction alors que dans de INSERTdéclaration?
sk
27

J'avais le même type de besoin et j'ai trouvé que cela fonctionnait bien pour moi (postgres 8.4):

CAST((COALESCE(myfield,'0')) AS INTEGER)

Quelques cas de test pour démontrer:

db=> select CAST((COALESCE(NULL,'0')) AS INTEGER);
 int4
------
    0
(1 row)

db=> select CAST((COALESCE('','0')) AS INTEGER);
 int4
------
    0
(1 row)

db=> select CAST((COALESCE('4','0')) AS INTEGER);
 int4
------
    4
(1 row)

db=> select CAST((COALESCE('bad','0')) AS INTEGER);
ERROR:  invalid input syntax for integer: "bad"

Si vous devez gérer la possibilité que le champ contienne du texte non numérique (tel que "100bad"), vous pouvez utiliser regexp_replace pour supprimer les caractères non numériques avant le cast.

CAST(REGEXP_REPLACE(COALESCE(myfield,'0'), '[^0-9]+', '', 'g') AS INTEGER)

Ensuite, les valeurs text / varchar comme "b3ad5" donneront également des nombres

db=> select CAST(REGEXP_REPLACE(COALESCE('b3ad5','0'), '[^0-9]+', '', 'g') AS INTEGER);
 regexp_replace
----------------
             35
(1 row)

Pour répondre à la préoccupation de Chris Cogdon avec la solution ne donnant pas 0 pour tous les cas, y compris un cas tel que «mauvais» (aucun caractère numérique du tout), j'ai fait cette déclaration ajustée:

CAST((COALESCE(NULLIF(REGEXP_REPLACE(myfield, '[^0-9]+', '', 'g'), ''), '0')) AS INTEGER);

Cela fonctionne de la même manière que les solutions les plus simples, sauf qu'il donnera 0 lorsque la valeur à convertir est uniquement des caractères non numériques, tels que "mauvais":

db=> select CAST((COALESCE(NULLIF(REGEXP_REPLACE('no longer bad!', '[^0-9]+', '', 'g'), ''), '0')) AS INTEGER);
     coalesce
----------
        0
(1 row)
Ghbarratt
la source
Pourquoi avez-vous besoin du «0» || ? D'après la documentation: "La fonction COALESCE renvoie le premier de ses arguments qui n'est pas nul." Donc, si vous avez null comme valeur, Coalesce s'en débarrassera.
Amala
@Amala True. Belle prise. Édité.
ghbarratt
1
La solution ne fonctionne que si l'entrée est un entier ou NULL. La question demandait de convertir n'importe quel type d'entrée et d'utiliser 0 si ce n'est pas convertible.
Chris Cogdon
@ChrisCogdon J'ai ajouté à la solution pour répondre à votre préoccupation de ne pas toujours donner zéro si la valeur à convertir est "non convertible". Cette version modifiée de la solution renverra 0 lorsqu'une chaîne sans caractère numérique est donnée comme valeur à convertir.
ghbarratt
22

C'est peut-être un peu un hack, mais cela a fait le travail dans notre cas:

(0 || myfield)::integer

Explication (testé sur Postgres 8.4):

L'expression mentionnée ci-dessus donne NULLdes valeurs NULL dans myfieldet 0pour des chaînes vides (ce comportement exact peut ou non correspondre à votre cas d'utilisation).

SELECT id, (0 || values)::integer from test_table ORDER BY id

Données de test:

CREATE TABLE test_table
(
  id integer NOT NULL,
  description character varying,
  "values" character varying,
  CONSTRAINT id PRIMARY KEY (id)
)

-- Insert Test Data
INSERT INTO test_table VALUES (1, 'null', NULL);
INSERT INTO test_table VALUES (2, 'empty string', '');
INSERT INTO test_table VALUES (3, 'one', '1');

La requête donnera le résultat suivant:

 ---------------------
 |1|null        |NULL|
 |2|empty string|0   |
 |3|one         |1   |
 ---------------------

Alors que sélectionner uniquement values::integerentraînera un message d'erreur.

J'espère que cela t'aides.

Mat
la source
3

SELECT CASE WHEN myfield="" THEN 0 ELSE myfield::integer END FROM mytable

Je n'ai jamais travaillé avec PostgreSQL mais j'ai vérifié dans le manuel la syntaxe correcte des instructions IF dans les requêtes SELECT.

Jan Hančič
la source
Cela fonctionne pour la table telle qu'elle est actuellement. J'ai un peu peur qu'à l'avenir, il puisse contenir des valeurs non numériques. J'aurais préféré une solution de type try / catch, mais cela fait l'affaire. Merci.
silviot
Vous pourriez peut-être utiliser des expressions régulières postgresql.org/docs/8.4/interactive/functions-matching.html mais cela pourrait être coûteux.
Acceptez
3

La réponse de @ Matthew est bonne. Mais cela peut être plus simple et plus rapide. Et la question demande de convertir des chaînes vides ( '') en 0, mais pas une autre entrée «syntaxe d'entrée non valide» ou «hors plage»:

CREATE OR REPLACE FUNCTION convert_to_int(text)
  RETURNS int AS
$func$
BEGIN
   IF $1 = '' THEN  -- special case for empty string like requested
      RETURN 0;
   ELSE
      RETURN $1::int;
   END IF;

EXCEPTION WHEN OTHERS THEN
   RETURN NULL;  -- NULL for other invalid input

END
$func$  LANGUAGE plpgsql IMMUTABLE;

Cela renvoie 0pour une chaîne vide et NULLpour toute autre entrée non valide.
Il peut facilement être adapté à toute conversion de type de données .

La saisie d'un bloc d'exception est nettement plus coûteuse. Si les chaînes vides sont courantes, il est judicieux de détecter ce cas avant de déclencher une exception.
Si les chaînes vides sont très rares, il vaut la peine de déplacer le test vers la clause d'exception.

Erwin Brandstetter
la source
1
CREATE OR REPLACE FUNCTION parse_int(s TEXT) RETURNS INT AS $$
BEGIN
  RETURN regexp_replace(('0' || s), '[^\d]', '', 'g')::INT;
END;
$$ LANGUAGE plpgsql;

Cette fonction retournera toujours 0s'il n'y a pas de chiffres dans la chaîne d'entrée.

SELECT parse_int('test12_3test');

reviendra 123

Oleg Mikhailov
la source
avez-vous effectué des tests de performances pour la fonction regex vs string? En outre, comment cela gère-t-il les valeurs nulles? Renvoyerait-il 0 ou NULL comme prévu? Merci!
vol7ron
1

J'ai trouvé le code suivant facile et fonctionnel. La réponse originale est ici https://www.postgresql.org/message-id/[email protected]

prova=> create table test(t text, i integer);
CREATE

prova=> insert into test values('123',123);
INSERT 64579 1

prova=> select cast(i as text),cast(t as int)from test;
text|int4
----+----
123| 123
(1 row)

J'espère que ça aide

Ashish Rana
la source
1

SUBSTRING peut aider dans certains cas, vous pouvez limiter la taille de l'int.

SELECT CAST(SUBSTRING('X12312333333333', '([\d]{1,9})') AS integer);
obsolète
la source
0

Si les données sont censées être des entiers et que vous n'avez besoin que de ces valeurs sous forme d'entiers, pourquoi ne pas parcourir tout le kilomètre et convertir la colonne en une colonne entière?

Ensuite, vous pouvez effectuer cette conversion de valeurs illégales en zéros une seule fois, au point du système où les données sont insérées dans la table.

Avec la conversion ci-dessus, vous obligez Postgres à convertir ces valeurs encore et encore pour chaque ligne de chaque requête pour cette table - cela peut sérieusement dégrader les performances si vous effectuez de nombreuses requêtes sur cette colonne de cette table.

Bandit
la source
En principe, vous avez raison, mais dans ce scénario particulier, je dois optimiser une seule requête lente dans une application. Je ne sais pas comment fonctionne le code qui gère l'entrée de données. Je ne veux pas y toucher. Jusqu'à présent, ma requête réécrite fonctionne, mais j'aimerais qu'elle ne soit pas interrompue dans des cas imprévus. Réorganiser l'application n'est pas une option, même si cela semble la chose la plus sensée.
silviot
0

La fonction suivante fait

  • utilisez une valeur par défaut ( error_result) pour les résultats non castables, par exemple abcou999999999999999999999999999999999999999999
  • garde nullcommenull
  • supprime les espaces et autres espaces en entrée
  • les valeurs exprimées comme valides bigintssont comparées lower_boundà, par exemple, appliquer uniquement des valeurs positives
CREATE OR REPLACE FUNCTION cast_to_bigint(text) 
RETURNS BIGINT AS $$
DECLARE big_int_value BIGINT DEFAULT NULL;
DECLARE error_result  BIGINT DEFAULT -1;
DECLARE lower_bound   BIGINT DEFAULT 0;
BEGIN
    BEGIN
        big_int_value := CASE WHEN $1 IS NOT NULL THEN GREATEST(TRIM($1)::BIGINT, lower_bound) END;
    EXCEPTION WHEN OTHERS THEN
        big_int_value := error_result;
    END;
RETURN big_int_value;
END;
Je 00 mÄ s
la source
-1

J'ai aussi le même besoin mais cela fonctionne avec JPA 2.0 et Hibernate 5.0.2:

SELECT p FROM MatchProfile p WHERE CONCAT(p.id, '') = :keyword

Fonctionne à merveille. Je pense que ça marche aussi avec LIKE.

Hendy Irawan
la source
-3

Cela devrait également faire le travail, mais cela concerne SQL et n'est pas spécifique à postgres.

select avg(cast(mynumber as numeric)) from my table
Ronak
la source