10

当我尝试同时更新多行时遇到问题。

这是我使用的表和查询(为便于阅读而简化):

桌子

CREATE TABLE foo
(
    pkid integer,
    x integer,
    y integer
)

询问

UPDATE foo SET x=t.x, y=t.y FROM
(VALUES (50, 50, 1),
        (100, 120, 2))
AS t(x, y, pkid) WHERE foo.pkid=t.pkid

此查询完美运行,但是当我尝试执行所有xy值为 null 的查询时,出现错误:

带空值的查询

UPDATE foo SET x=t.x, y=t.y FROM
(VALUES (null, 20, 1),
        (null, 50, 2))
AS t(x, y, pkid) WHERE foo.pkid=t.pkid

错误

ERROR:  column "x" is of type integer but expression is of type text
LINE 1: UPDATE foo SET x=t.x FROM

解决此问题的唯一方法是将至少一个值更改为(null, 20, 1)(null:int, 50, 2)但我不能这样做,因为我有一个生成这些“更新多行”查询的函数,它对列类型一无所知。

这里最好的解决方案是什么?是否有更好的多行更新查询?有没有类似的函数或语法AS t(x:gettype(foo.x), y:gettype(foo.y), pkid:gettype(foo.pkid))

4

3 回答 3

16

使用独立VALUES表达式 PostgreSQL 不知道数据类型应该是什么。使用简单的数字文字,系统很乐意假设匹配类型。但是对于其他输入(如NULL),您需要显式转换 - 正如您已经发现的那样。

您可以查询pg_catalog(快速,但特定于 PostgreSQL)或information_schema(慢,但标准 SQL)来找出并准备具有适当类型的语句。

或者你可以使用这些简单的“技巧”之一(我把最好的留到最后):

0. 用 选择行LIMIT 0,用 追加行 UNION ALL VALUES

UPDATE foo f
SET    x = t.x
     , y = t.y
FROM  (
  (SELECT pkid, x, y FROM foo LIMIT 0) -- parenthesis needed with LIMIT
   UNION ALL
   VALUES
      (1, 20, NULL)  -- no type casts here
    , (2, 50, NULL)
   ) t               -- column names and types are already defined
WHERE  f.pkid = t.pkid;

子查询的第一个子选择:

(SELECT x, y, pkid  FROM foo LIMIT 0)

获取列的名称和类型,但LIMIT 0阻止它添加实际行。随后的行被强制转换为现在定义良好的行类型 - 并立即检查它们是否匹配该类型。应该是对原始表单的细微改进。

在为表的所有列提供值时,此简短语法可用于第一行:

(TABLE foo LIMIT 0)

主要限制:PostgresVALUES立即将独立表达式的输入文字转换为“尽力而为”类型。当它稍后尝试转换为 first 的给定类型时,SELECT如果在假定类型和目标类型之间没有注册的赋值转换,对于某些类型来说可能已经太晚了。示例:text->timestamptext-> json

临:

  • 最小开销。
  • 可读、简单、快速。
  • 您只需要知道表的相关列名。

缺点:

  • 某些类型的类型解析可能会失败。

1. 用 选择行LIMIT 0,用 追加行UNION ALL SELECT

UPDATE foo f
SET    x = t.x
     , y = t.y
FROM  (
  (SELECT pkid, x, y FROM foo LIMIT 0) -- parenthesis needed with LIMIT
   UNION ALL SELECT 1, 20, NULL
   UNION ALL SELECT 2, 50, NULL
   ) t               -- column names and types are already defined
WHERE  f.pkid = t.pkid;

临:

  • 0.,但避免类型解析失败。

缺点:

  • UNION ALL SELECTVALUES正如您在测试中发现的那样,它比长行列表的表达式慢。
  • 每行的详细语法。

2.VALUES每列类型的表达式

...
FROM  (
   VALUES 
     ((SELECT pkid FROM foo LIMIT 0)
    , (SELECT x    FROM foo LIMIT 0)
    , (SELECT y    FROM foo LIMIT 0))  -- get type for each col individually
   , (1, 20, NULL)
   , (2, 50, NULL)
   ) t (pkid, x, y)  -- columns names not defined yet, only types.
...

0相反。这避免了过早的类型解析。

VALUES表达式中的第一行是一行NULL值,它定义了所有后续行的类型。这个领先的噪音行WHERE f.pkid = t.pkid后来被过滤掉了,所以它永远不会出现。OFFSET 1出于其他目的,您可以在子查询中消除添加的第一行。

临:

  • 通常比1.(甚至0.)快
  • 具有许多列且只有少数列相关的表的简短语法。
  • 您只需要知道表的相关列名。

缺点:

  • 仅几行的详细语法
  • 可读性较差(IMO)。

3.VALUES行类型表达式

UPDATE foo f
SET x = (t.r).x         -- parenthesis needed to make syntax unambiguous
  , y = (t.r).y
FROM (
   VALUES
      ('(1,20,)'::foo)  -- columns need to be in default order of table
     ,('(2,50,)')       -- nothing after the last comma for NULL
   ) t (r)              -- column name for row type
WHERE  f.pkid = (t.r).pkid;

你显然知道表名。如果您还知道列数及其顺序,则可以使用它。

对于 PostgreSQL 中的每个表,都会自动注册一个行类型。如果您匹配表达式中的列数,您可以转换为表的行类型 ( '(1,50,)'::foo),从而隐式分配列类型。在逗号后面不添加任何内容以输入NULL值。为每个不相关的尾随列添加一个逗号。
在下一步中,您可以使用演示的语法访问各个列。手册中有关字段选择的更多信息。

或者您可以添加一行 NULL 值并对实际数据使用统一语法:

...
  VALUES
      ((NULL::foo))  -- row of NULL values
    , ('(1,20,)')    -- uniform ROW value syntax for all
    , ('(2,50,)')
...

临:

  • 最快(至少在我的测试中,行和列很少)。
  • 需要所有列的少数行或表的最短语法。
  • 您不必拼出表格的列 - 所有列都会自动具有匹配的名称。

缺点:

  • 从记录/行/复合类型中选择字段的语法不太为人所知。
  • 您需要按默认顺序知道相关列的数量和位置。

4.具有分解行类型VALUES的表达式

3.类似,但采用标准语法分解行:

UPDATE foo f
SET    x = t.x
     , y = t.y
FROM (
   VALUES
      (('(1,20,)'::foo).*)  -- decomposed row of values
    , (2, 50, NULL)
   ) t(pkid, x, y)  -- arbitrary column names (I made them match)
WHERE  f.pkid = t.pkid;     -- eliminates 1st row with NULL values

或者,再次使用 NULL 值的前导行:

...
   VALUES
      ((NULL::foo).*)  -- row of NULL values
    , (1, 20, NULL)    -- uniform syntax for all
    , (2, 50, NULL)
...

3.一样的优点和缺点,但使用更常见的语法。
你需要拼出列名(如果你需要的话)。

5.VALUES从行类型中获取类型的表达式

就像Unril 评论的那样,我们可以结合2.4.的优点来仅提供列的一个子集:

UPDATE foo f
SET   (  x,   y)
    = (t.x, t.y)  -- short notation, see below
FROM (
   VALUES
      ((NULL::foo).pkid, (NULL::foo).x, (NULL::foo).y)  -- subset of columns
    , (1, 20, NULL)
    , (2, 50, NULL)
   ) t(pkid, x, y)       -- arbitrary column names (I made them match)
WHERE  f.pkid = t.pkid;

4.一样的优点和缺点,但我们可以使用任何列子集,而不必知道完整列表。

还显示其UPDATE本身的简短语法,这对于具有多列的情况很方便。有关的:

4. 和 5. 是我的最爱。

db<>fiddle here - 演示所有

于 2012-09-14T15:19:53.547 回答
2

如果您有一个生成查询的脚本,您可以提取和缓存每列的数据类型,并相应地创建类型转换。例如:

SELECT column_name,data_type,udt_name 
FROM information_schema.columns 
WHERE table_name = 'foo';

从这个 udt_name 中,您将获得必要的演员表,正如您在上一段中解释的那样。此外,您可以这样做:

UPDATE foo
SET x = t.x
FROM (VALUES(null::int4,756),(null::int4,6300))
AS t(x,pkid)
WHERE foo.pkid = t.pkid;
于 2012-09-14T14:53:08.737 回答
0

您的脚本将从 foo 创建一个临时表。它将具有与 foo 相同的数据类型。使用不可能的条件,所以它是空的:

select x, y, pkid
into temp t
from foo
where pkid = -1

使您的脚本插入其中:

insert into t (x, y, pkid) values
(null, 20, 1),
(null, 50, 2)

现在从它更新:

update foo 
set x=t.x, y=t.y 
from t
where foo.pkid=t.pkid

最后放下它:

drop table t
于 2012-09-14T15:14:10.337 回答