Postgres devuelve un valor predeterminado cuando una columna no existe
Frecuentes
Visto 12,273 veces
17
I have a query where I essentially want a fallback value if a certain column is missing. I was wondering if I can handle this purely in my query (rather than probing first and sending a seperate query. In essence i'm looking for an equivalent to COALESCE
that handles the case of a missing column.
Imagine the following 2 tables.
T1
id | title | extra
1 A | value
- and -
T2
id | title
1 A
I'd like to be able to SELECT from either of these tables WITH THE SAME QUERY.
eg, if t2 actually had an 'extra' column I could use
SELECT id,title, COALESCE(extra, 'default') as extra
But that only works if the column value is NULL, not when the column is missing entirely.
I would prefer an SQL version but I can accept a PLPGSQL function (with a behaviour similiar to COALLESCE) too.
NOTE to SQL purists: I don't really feel like debating why I want to do this in SQL and not in application logic (or why I won't just add the column permanently to the schema) so please restrict your comments/answers to the specific request and not your opinion on database 'correctness' or whatever else might offend you about this question.
2 Respuestas
22
Por que Rowan's hack work (mostly)?
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)
Normally, it would not work at all. Postgres parses the SQL statement and throws an exception if cualquier of the involved columns does not exist.
The trick is to introduce a table name (or alias) with the same name as the column name in question. extra
in this case. Every table name can be referenced as a whole, which results in the whole row being returned as type record
. And since every type can be cast to text
, we can cast this whole record to text
. This way, Postgres accepts the query as valid.
Since column names take precedence over table names, extra::text
is interpreted to be the column tbl.extra
if the column exists. Otherwise, it would default to returning the whole row of the table extra
- which never happens.
Try to pick a different table alias for extra
para ver por ti mismo
Esta es una undocumented hack and might break if Postgres decides to change the way SQL strings are parsed and planned in future versions - even though unlikely.
Sin ambigüedades
If you decide to use this, at least make it unambiguous.
A table name alone is not unique. A table named "tbl" can exist any number of times in multiple schemas of the same database, which could lead to very confusing and completely false results. You necesita to supply the schema name additionally:
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;
Más rápido
Since this query is hardly portable to other RDBMS, I suggest to use the tabla de catalogo pg_attribute
instead of the information schema view information_schema.columns
. About 10 times faster.
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);
También usando el more convenient and secure cast to regclass
. Ver:
You can attach the needed alias to fool Postgres to cualquier table, including the primary table itself. You don't need to join to another relation at all, which should be fastest:
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;
Conveniencia
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!)';
Simplifies the query to:
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);
Using the form with additional relation here, since it turned out to be faster with the function.
Still, you only get the representación de texto of the column with any of these queries. It's not as simple to get the tipo real.
punto de referencia
I ran a quick benchmark with 100k rows on pg 9.1 and 9.2 to find these to be fastest:
Lo más 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;
2nd fastest:
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);
Respondido el 18 de enero de 22 a las 13:01
+1 @ErwinBrandstetter great as always, I had no time at work to look closer at the query to understand why it works, and totally forgot about this question when I came home, thanks for explanation - Pekar romano
Great explanation, I leant something. - Serbal
Excellent details. I didn't even understand why you were calling it a hack until you explained the column vs. table thing. Luckily for me I have no need for SQL compliance with other DBMS and I can pick and choose my Postgres version so all of your methods/info are useful. - Empalme
7
One way is to look up the information schema table and do a little magic with it.
Algo como:
SELECT id, title, CASE WHEN extra_exists THEN extra ELSE 'default' END AS extra
FROM mytable
CROSS JOIN (
SELECT EXISTS (SELECT 1
FROM information_schema.columns
WHERE table_name='mytable' AND column_name='extra') AS extra_exists) extra
Edit: Where 'mytable' needs to be passed in for the table you want to query.
Respondido el 23 de Septiembre de 13 a las 16:09
ERROR: input of anonymous composite types is not implemented LINE 1: ...id, title, CASE WHEN extra_exists THEN extra ELSE 'default' ... (Postgres 8.4) - Empalme
Looks like you need to cast as well, this change works: THEN "extra"::text ELSE 'default'::text
- Empalme
+1 Didn't know that PostgreSQL so tolerant. SQL will raise an error if I'll try to do query like that. Here's SQL fiddle to see it's working BTW - sqlfiddle.com/index.cfm?_escaped_fragment_=2/d41d8/9720#!12/… - Pekar romano
The reason why this hack happens to work is not disclosed, which makes it dangerous and possibly very annoying. It also does not work at all without the explicit cast like SpliFF mentions. - Erwin Brandstetter
@RomanPekar: Postgres is no as tolerant as this answer makes it seem. I added an explanation. - Erwin Brandstetter
No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas sql postgresql plpgsql or haz tu propia pregunta.
Does the plpgsql tag suggest that you'd be happy to use a function instead of the table? - mu is too short
I mean I would accept a function that works like COALESCE but for missing fields, not NULLS. - SpliFF
@mu. Thanks, that makes sense though I have had success with similiar 'subselect' functions provided I pass the table name as a 'regclass' argument to the function,
SELECT myFunc('mytable','mycol','mydefaultvalue') AS extra
should work when the function is defined asmyFunc(IN _tbl regclass, IN _col regclass, IN default text)
. - SpliFF