PostgreSQL
 sql >> Base de Dados >  >> RDS >> PostgreSQL

Postgres retorna um valor padrão quando uma coluna não existe

Por que o hack de Rowan trabalha (principalmente)?

SELECT id, title
     , CASE WHEN extra_exists THEN extra::text ELSE 'default' END AS extra
FROM   tbl
CROSS  JOIN (
   SELECT EXISTS (
      SELECT FROM information_schema.columns 
      WHERE  table_name = 'tbl'
      AND    column_name = 'extra')
   ) AS extra(extra_exists)

Normalmente, não funcionaria de jeito nenhum. O Postgres analisa a instrução SQL e lança uma exceção se qualquer das colunas envolvidas não existe.

O truque é introduzir um nome de tabela (ou alias) com o mesmo nome do nome da coluna em questão. extra nesse caso. Cada nome de tabela pode ser referenciado como um todo, o que resulta no retorno de toda a linha como tipo record . E como todos os tipos podem ser convertidos em text , podemos converter todo esse registro em text . Dessa forma, o Postgres aceita a consulta como válida.

Como os nomes das colunas têm precedência sobre os nomes das tabelas, extra::text é interpretado como a coluna tbl.extra se a coluna existir. Caso contrário, o padrão seria retornar toda a linha da tabela extra - o que nunca acontece.

Tente escolher um alias de tabela diferente para extra para ver por si mesmo.

Este é um hack não documentado e pode quebrar se o Postgres decidir mudar a maneira como as strings SQL são analisadas e planejadas em versões futuras - mesmo que isso pareça improvável.

Sem ambiguidade


Se você decidir usar isso, pelo menos torne-o inequívoco .

Um nome de tabela sozinho não é exclusivo. Uma tabela chamada "tbl" pode existir várias vezes em vários esquemas do mesmo banco de dados, o que pode levar a resultados muito confusos e completamente falsos. Você precisa para fornecer o nome do esquema adicionalmente:
SELECT id, title
     , CASE WHEN col_exists THEN extra::text ELSE 'default' END AS extra
FROM   tbl
CROSS  JOIN (
   SELECT EXISTS (
      SELECT FROM information_schema.columns 
      WHERE  table_schema = 'public'
      AND    table_name = 'tbl'
      AND    column_name = 'extra'
      ) AS col_exists
   ) extra;

Mais rápido


Como essa consulta dificilmente é portátil para outros RDBMS, sugiro usar o tabela de catálogo pg_attribute em vez da visualização do esquema de informações information_schema.columns . Cerca de 10 vezes mais rápido.
SELECT id, title
     , CASE WHEN col_exists THEN extra::text ELSE 'default' END AS extra
FROM   tbl
CROSS  JOIN (
   SELECT EXISTS (
      SELECT FROM pg_catalog.pg_attribute
      WHERE  attrelid = 'myschema.tbl'::regclass  -- schema-qualified!
      AND    attname  = 'extra'
      AND    NOT attisdropped    -- no dropped (dead) columns
      AND    attnum   > 0        -- no system columns
      )
   ) extra(col_exists);

Também usando a transmissão mais conveniente e segura para regclass . Ver:

Você pode anexar o alias necessário para enganar o Postgres para qualquer tabela, incluindo a própria tabela primária. Você não precisa se juntar a outra relação, o que deve ser mais rápido:
SELECT id, title
     , CASE WHEN EXISTS (SELECT FROM pg_catalog.pg_attribute
                         WHERE  attrelid = 'tbl'::regclass
                         AND    attname  = 'extra'
                         AND    NOT attisdropped
                         AND    attnum   > 0)
            THEN extra::text
            ELSE 'default' END AS extra
FROM   tbl AS extra;

Conveniência

You could encapsulate the test for existence in a simple SQL function (once), arriving (almost) at the function you have been asking for:

CREATE OR REPLACE FUNCTION col_exists(_tbl regclass, _col text)
  RETURNS bool
  LANGUAGE sql STABLE AS
$func$
SELECT EXISTS (
   SELECT FROM pg_catalog.pg_attribute
   WHERE  attrelid = $1
   AND    attname  = $2
   AND    NOT attisdropped
   AND    attnum   > 0
   )
$func$;

COMMENT ON FUNCTION col_exists(regclass, text) IS
'Test for existence of a column. Returns TRUE / FALSE.
$1 .. exact table name (case sensitive!), optionally schema-qualified
$2 .. exact column name (case sensitive!)';

Simplifica a consulta para:
SELECT id, title
     , CASE WHEN col_exists THEN extra::text ELSE 'default' END AS extra
FROM   tbl
CROSS  JOIN col_exists('tbl', 'extra') AS extra(col_exists);

Usando o formulário com relação adicional aqui, já que ficou mais rápido com a função.

Ainda assim, você obtém apenas a representação de texto da coluna com qualquer uma dessas consultas. Não é tão simples obter o tipo real .

Referência


Eu executei um benchmark rápido com 100k linhas nas páginas 9.1 e 9.2 para descobrir que elas são as mais rápidas:

O mais rápido:
SELECT id, title
     , CASE WHEN EXISTS (SELECT FROM pg_catalog.pg_attribute
                         WHERE  attrelid = 'tbl'::regclass
                         AND    attname  = 'extra'
                         AND    NOT attisdropped
                         AND    attnum   > 0)
            THEN extra::text
            ELSE 'default' END AS extra
FROM   tbl AS extra;

2º mais rápido:
SELECT id, title
     , CASE WHEN col_exists THEN extra::text ELSE 'default' END AS extra
FROM   tbl
CROSS  JOIN col_exists('tbl', 'extra') AS extra(col_exists);

db<>fiddle aqui
Antigo sqlfiddle