【问题标题】:Casting NULL type when updating multiple rows更新多行时强制转换 NULL 类型
【发布时间】:2012-09-07 17:27:39
【问题描述】:

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

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

表格

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))的函数或语法?

【问题讨论】:

    标签: sql postgresql types casting sql-update


    【解决方案1】:

    使用独立的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)
    

    主要限制:Postgres 立即将独立VALUES 表达式的输入文字转换为“尽力而为”类型。当它稍后尝试转换为第一个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 值。为每个不相关的尾随列添加一个逗号。
    在下一步中,您可以使用演示的语法访问各个列。更多关于Field Selection in the manual

    或者您可以添加一行 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 commented一样,我们可以结合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. 是我的最爱。

    dbfiddle here - 全部演示

    【讨论】:

    • 它不起作用:update foo SET x=t.x, y=t.y FROM ( (SELECT x, y, pkid FROM foo LIMIT 0) UNION ALL VALUES (NULL,5, 1),(NULL,2, 2)) t WHERE foo.pkid=t.pkidUNION types integer and text cannot be matched。它与我在问题中发布的问题相同:(
    • @AlbertFerras:UNION ALL SELECT 处理大量行时速度较慢。 (我从您的其他评论中看到:您有数千个)。我添加了两个更快的替代方案,各有利弊。现在对你来说应该很快。
    • @AlbertFerras 如果这是在后台批处理中运行,那么几秒钟甚至几分钟都不能证明(非常好的)jspboix 建议中更复杂的方法是合理的。我认为 KISS 原则在这里获胜。
    • 第二版运行完美,速度与我之前的一样 :) 因为我不需要进行新查询来获取列类型(并将它们缓存在我的脚本等中)。 ) 这将是公认的答案。如果有一天我需要更快的东西,我会尝试你提出的第三个版本。谢谢你:)
    • 您也可以使用values ((null::test.city).id, (null::test.city).name), (2, 'City name') 语法代替(select id from test.city limit 0) 子查询。
    【解决方案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;
    

    【讨论】:

    • 我喜欢这个解决方案,因为我只需要对所有更新多行进行 1 次查询。现在,如果没有更好的结果,我接受这个:)
    【解决方案3】:

    您的脚本将从 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
    

    【讨论】:

    • 如果我运行更新多行一次会很好,但我会运行 1000 个查询,每个查询约 2000 行。我不喜欢为这 1000 个查询中的每一个创建和删除一个表的想法。另外,我不想拥有prepare_update(), .. updates ..., end_update(),因为它会使代码更复杂,而且我也需要更改很多以前的代码。
    猜你喜欢
    • 1970-01-01
    • 2021-01-24
    • 2022-01-13
    • 1970-01-01
    • 1970-01-01
    • 2015-08-17
    • 1970-01-01
    • 1970-01-01
    • 2014-10-19
    相关资源
    最近更新 更多