16

我有一个查询,如果缺少某个列,我基本上想要一个后备值。我想知道我是否可以纯粹在我的查询中处理这个问题(而不是先探测并发送单独的查询。本质上,我正在寻找一个与COALESCE处理缺失列的情况相同的等价物。

想象一下以下 2 个表。

T1
id | title | extra
1    A     | value

- and -

T2
id | title
1    A

我希望能够使用相同的查询从这些表中的任何一个中进行选择。

例如,如果 t2 实际上有一个“额外”列,我可以使用

 SELECT id,title, COALESCE(extra, 'default') as extra

但这仅在列值为 NULL 时才有效,而不是在列完全丢失时有效。

我更喜欢 SQL 版本,但我也可以接受 PLPGSQL 函数(具有类似于 COALLESCE 的行为)。

对 SQL 纯粹主义者的注意:我真的不想争论为什么要在 SQL 中而不是在应用程序逻辑中执行此操作(或者为什么我不会将列永久添加到架构中),因此请将您的评论/答案限制为具体要求,而不是您对数据库“正确性”的看法或其他任何可能会冒犯您的问题。

4

2 回答 2

22

为什么Rowan 的 hack(大部分)有效?

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)

通常,它根本不起作用。Postgres 解析 SQL 语句并在任何涉及的列不存在时抛出异常。

诀窍是引入与相关列名同名的表名(或别名)。extra在这种情况下。每个表名都可以作为一个整体引用,这导致整行返回为 type record。而且由于每种类型都可以转换为text,我们可以将整个记录转换为text. 这样,Postgres 将查询视为有效。

由于列名优先于表名,因此如果该列存在,则将其extra::text解释为该列。tbl.extra否则,它将默认返回表的整行extra——这永远不会发生。

尝试选择不同的表别名extra以供您自己查看。

这是一个未记录的 hack,如果 Postgres 决定在未来的版本中改变 SQL 字符串的解析和计划方式,它可能会中断——即使不太可能。

明确的

如果您决定使用它,至少要明确

单独的表名不是唯一的。一个名为“tbl”的表可以在同一个数据库的多个模式中存在任意多次,这可能会导致非常混乱和完全错误的结果。您需要另外提供架构名称:

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;

快点

由于这个查询很难移植到其他 RDBMS,我建议使用目录表pg_attribute而不是信息模式视图information_schema.columns。大约快 10 倍。

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

还使用了更方便、更安全的cast toregclass。看:

您可以附加所需的别名来欺骗 Postgres 到任何表,包括主表本身。您根本不需要加入另一个关系,这应该是最快的:

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;

方便

您可以将测试是否存在封装在一个简单的 SQL 函数中(一次),(几乎)到达您一直要求的函数:

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

将查询简化为:

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

在此处使用具有附加关系的表单,因为使用该功能会更快。

不过,您只能通过任何这些查询获得该列的文本表示形式。获取实际类型并不是那么简单。

基准

我在 pg 9.1 和 9.2 上运行了一个包含 100k 行的快速基准测试,以发现这些是最快的:

最快的:

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;

第二快:

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 here
sqlfiddle

于 2013-09-23T16:16:03.253 回答
6

一种方法是查找信息模式表并用它做一些魔术。

就像是:

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

编辑:需要为要查询的表传入“mytable”的位置。

于 2013-09-23T04:29:25.430 回答