Como converter uma string para número inteiro e ter 0 em caso de erro na conversão com o PostgreSQL?

128

No PostgreSQL, tenho uma tabela com uma coluna varchar. Os dados devem ser inteiros e eu preciso deles no tipo inteiro em uma consulta. Alguns valores são cadeias vazias. Os seguintes:

SELECT myfield::integer FROM mytable

rendimentos ERROR: invalid input syntax for integer: ""

Como posso consultar um elenco e ter 0 em caso de erro durante o elenco no postgres?

silviot
fonte

Respostas:

161

Eu mesmo estava lutando com um problema semelhante, mas não queria a sobrecarga de uma função. Eu vim com a seguinte consulta:

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

O Postgres atalhos suas condicionais, portanto você não deve ter nenhum número inteiro atingindo sua conversão :: integer. Ele também lida com valores NULL (eles não correspondem ao regexp).

Se você deseja zeros em vez de não selecionar, uma instrução CASE deve funcionar:

SELECT CASE WHEN myfield~E'^\\d+$' THEN myfield::integer ELSE 0 END FROM mytable;
Anthony Briggs
fonte
14
Eu recomendaria fortemente seguir a sugestão de Matthew. Esta solução tem problemas com cadeias que parecem números, mas são maiores que o valor máximo que você pode colocar em um número inteiro.
pilif
4
i comentário do segundo piloto. esse valor máximo é um bug esperando para acontecer. o ponto de não gerar um erro é não gerar um erro quando os dados são inválidos. esta resposta aceita NÃO resolve isso. obrigado Matthew! Ótimo trabalho!
Shawn Kovac
3
Por melhor que a resposta de Matthew seja, eu só precisava de uma maneira rápida e suja de lidar com a verificação de alguns dados. Também admito que meu conhecimento está faltando agora na definição de funções no SQL. Eu estava interessado apenas em números entre 1 e 5 dígitos, então mudei o regex para E'\\d{1,5}$'.
Bobort
3
Sim, sim, esta solução é relativamente rápida e suja, mas no meu caso eu sabia quais dados eu tinha e que a tabela era relativamente curta. É muito mais fácil do que escrever (e depurar) uma função inteira. @ O {1,5}limite de Bobort acima nos dígitos é possivelmente uma boa idéia se você estiver preocupado com o estouro, mas mascarará números maiores, o que pode causar problemas se você estiver convertendo uma tabela. Pessoalmente, prefiro ter o erro de consulta antecipadamente e saber que alguns dos meus "números inteiros" são estranhos (você também pode selecionar com o E'\\d{6,}$'primeiro para ter certeza).
Anthony Briggs
1
@ Anthony Briggs: Isso não funcionará se o meu campo contiver "'" ou "," ou "." Ou "-' #
Stefan Steiger
100

Você também pode criar sua própria função de conversão, dentro da qual você pode usar blocos de exceção:

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;

Teste:

=# 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
fonte
8
ao contrário da resposta aceita, esta solução aqui é mais correta, pois também pode lidar com números grandes demais para caberem em um número inteiro e também é provável que seja mais rápida, pois não funciona de validação no caso comum (= cadeias de caracteres válidas )
pilif
Como você converteria string em número inteiro em campos específicos usando sua função while in in INSERTstatement?
sk
27

Eu tinha o mesmo tipo de necessidade e achei que isso funcionava bem para mim (postgres 8.4):

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

Alguns casos de teste para demonstrar:

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"

Se você precisar lidar com a possibilidade de o campo ter texto não numérico (como "100bad"), você pode usar regexp_replace para eliminar caracteres não numéricos antes da conversão.

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

Valores de texto / varchar como "b3ad5" também fornecerão números

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

Para abordar a preocupação de Chris Cogdon com a solução de não dar 0 para todos os casos, incluindo um caso como "ruim" (sem caracteres de dígito), fiz esta declaração ajustada:

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

Funciona de maneira semelhante às soluções mais simples, exceto que dará 0 quando o valor a ser convertido for apenas caracteres que não sejam dígitos, como "incorreto":

db=> select CAST((COALESCE(NULLIF(REGEXP_REPLACE('no longer bad!', '[^0-9]+', '', 'g'), ''), '0')) AS INTEGER);
     coalesce
----------
        0
(1 row)
ghbarratt
fonte
Por que você precisa do '0' || ? Dos documentos: "A função COALESCE retorna o primeiro de seus argumentos que não são nulos". Portanto, se você tiver nulo como valor, o Coalesce se livrará dele.
Amala
@Amala True. Boa pegada. Editado.
precisa saber é o seguinte
1
A solução funciona apenas se a entrada for um número inteiro ou NULL. A pergunta estava pedindo para converter qualquer tipo de entrada e usar 0 se não for conversível.
Chris Cogdon
@ChrisCogdon Eu adicionei à solução para resolver sua preocupação em nem sempre dar zero se o valor da conversão for "não convertível". Esta versão aprimorada da solução retornará 0 quando uma sequência sem caracteres de dígito for fornecida como o valor a ser convertido.
precisa saber é o seguinte
22

Isso pode ser um pouco complicado, mas o trabalho foi feito no nosso caso:

(0 || myfield)::integer

Explicação (Testada no Postgres 8.4):

A expressão acima mencionada gera NULLvalores NULL em myfielde 0para cadeias vazias (esse comportamento exato pode ou não se encaixar no seu caso de uso).

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

Dados de teste:

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

A consulta produzirá o seguinte resultado:

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

Considerando que somente selecionar values::integerresultará em uma mensagem de erro.

Espero que isto ajude.

Matt
fonte
3

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

Eu nunca trabalhei com o PostgreSQL, mas verifiquei o manual quanto à sintaxe correta das instruções IF nas consultas SELECT.

Jan Hančič
fonte
Isso funciona para a mesa como está agora. Estou com um pouco de medo de que no futuro possa conter valores não numéricos. Eu teria preferido uma solução try / catch-like, mas isso funciona. Obrigado.
silviot
Talvez você possa usar expressões regulares postgresql.org/docs/8.4/interactive/functions-matching.html, mas isso pode ser caro. Também aceitar a resposta se é a solução :)
Jan Hančič
3

@ A resposta de Matthew é boa. Mas pode ser mais simples e rápido. E a pergunta pede para converter strings vazias ( '') em 0, mas não em outras "sintaxe de entrada inválida" ou "fora do intervalo":

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;

Isso retorna 0para uma sequência vazia e NULLpara qualquer outra entrada inválida.
Pode ser facilmente adaptado para qualquer conversão de tipo de dados .

Inserir um bloco de exceção é substancialmente mais caro. Se cadeias vazias são comuns , faz sentido capturar esse caso antes de gerar uma exceção.
Se cadeias vazias forem muito raras, vale a pena mover o teste para a cláusula de exceção.

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

Esta função sempre retornará 0se não houver dígitos na sequência de entrada.

SELECT parse_int('test12_3test');

retornará 123

Oleg Mikhailov
fonte
você executou algum teste de desempenho para a função regex vs string? Além disso, como isso lida com nulos? Ele retornaria 0 ou NULL conforme o esperado? Obrigado!
vol7ron
1

Achei o código a seguir fácil e funcional. A resposta original está aqui 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)

espero que ajude

Ashish Rana
fonte
1

SUBSTRING pode ajudar em alguns casos, você pode limitar o tamanho do int.

SELECT CAST(SUBSTRING('X12312333333333', '([\d]{1,9})') AS integer);
descontinuada
fonte
0

Se os dados deveriam ser números inteiros e você só precisa desses valores como números inteiros, por que não percorre a milha inteira e converte a coluna em uma coluna inteira?

Em seguida, você pode fazer essa conversão de valores ilegais em zeros apenas uma vez, no ponto do sistema em que os dados são inseridos na tabela.

Com a conversão acima, você está forçando o Postgres a converter esses valores repetidamente para cada linha única em cada consulta da tabela - isso pode prejudicar seriamente o desempenho se você fizer muitas consultas nessa coluna nesta tabela.

Bandido
fonte
Em princípio, você está certo, mas nesse cenário específico, tenho que otimizar uma única consulta lenta em um aplicativo. Não sei como funciona o código que lida com a entrada de dados. Eu não quero tocar. Até agora, minha consulta reescrita funciona, mas eu gostaria que ela não ocorresse em casos imprevistos. Re-arquitetar o aplicativo não é uma opção, mesmo que pareça a coisa mais sensata.
silviot
0

A seguinte função não

  • use um valor padrão ( error_result) para resultados não determináveis, por exemplo, abcou999999999999999999999999999999999999999999
  • mantém nullcomonull
  • apara os espaços e outros espaços em branco na entrada
  • valores expressos como válidos bigintssão comparados lower_boundcom, por exemplo, impor apenas valores positivos
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;
Th 00 seg
fonte
-1

Eu também tenho a mesma necessidade, mas isso funciona com o JPA 2.0 e o Hibernate 5.0.2:

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

Faz maravilhas. Eu acho que funciona com o LIKE também.

Hendy Irawan
fonte
-3

Isso também deve fazer o trabalho, mas é através do SQL e não do postgres específico.

select avg(cast(mynumber as numeric)) from my table
ronak
fonte