如何插入包含外键的行?


54

使用PostgreSQL v9.1。我有以下表格:

CREATE TABLE foo
(
    id BIGSERIAL     NOT NULL UNIQUE PRIMARY KEY,
    type VARCHAR(60) NOT NULL UNIQUE
);

CREATE TABLE bar
(
    id BIGSERIAL NOT NULL UNIQUE PRIMARY KEY,
    description VARCHAR(40) NOT NULL UNIQUE,
    foo_id BIGINT NOT NULL REFERENCES foo ON DELETE RESTRICT
);

假设第一个表foo是这样填充的:

INSERT INTO foo (type) VALUES
    ( 'red' ),
    ( 'green' ),
    ( 'blue' );

有什么方法bar可以通过引用foo表轻松地将行插入其中?还是我必须分两步进行操作,首先查找所需的foo类型,然后在其中插入新行bar

这是一个伪代码示例,显示我希望做的事:

INSERT INTO bar (description, foo_id) VALUES
    ( 'testing',     SELECT id from foo WHERE type='blue' ),
    ( 'another row', SELECT id from foo WHERE type='red'  );

Answers:


67

您的语法几乎是不错的,需要在子查询周围加上括号,并且可以使用:

INSERT INTO bar (description, foo_id) VALUES
    ( 'testing',     (SELECT id from foo WHERE type='blue') ),
    ( 'another row', (SELECT id from foo WHERE type='red' ) );

SQL-Fiddle上测试

换句话说,如果您要插入很多值,则使用较短的语法:

WITH ins (description, type) AS
( VALUES
    ( 'more testing',   'blue') ,
    ( 'yet another row', 'green' )
)  
INSERT INTO bar
   (description, foo_id) 
SELECT 
    ins.description, foo.id
FROM 
  foo JOIN ins
    ON ins.type = foo.type ;

读了几次,但我现在知道您提供的第二个解决方案。我喜欢。系统刚启动时,现在就使用它来引导我的数据库以几个已知值。
斯特凡

37

普通插入

INSERT INTO bar (description, foo_id)
SELECT val.description, f.id
FROM  (
   VALUES
      (text 'testing', text 'blue')  -- explicit type declaration; see below
    , ('another row', 'red' )
    , ('new row1'   , 'purple')      -- purple does not exist in foo, yet
    , ('new row2'   , 'purple')
   ) val (description, type)
LEFT   JOIN foo f USING (type);
  • 使用LEFT [OUTER] JOIN而不是[INNER] JOIN表示在中找不到匹配项时val ,不会删除 from foo。而是NULL输入foo_id

  • VALUES子查询表达式不一样@ ypercube的 CTE。公用表表达式提供了附加功能,在大型查询中更易于阅读,但它们也构成了优化障碍。因此,当不需要上述任何一项时,子查询通常会更快一些。

  • id因为列名是广泛使用的反模式。应该是foo_idand bar_id或任何描述性内容。当加入一堆表时,最终会得到多个列,所有列都命名为id...

  • 考虑使用普通textvarchar代替varchar(n)。如果确实需要施加长度限制,请添加一个CHECK约束:

  • 您可能需要添加显式类型转换。由于VALUES表达式未直接附加到表(如中的INSERT ... VALUES ...),因此无法派生类型,并且在没有显式类型声明的情况下使用默认数据类型,这在所有情况下可能都不起作用。在第一行就足够了,其余的将排成一行。

同时插入缺少的FK行

如果要动态创建不存在的条目foo,请在单个SQL语句中使用CTE:

WITH sel AS (
   SELECT val.description, val.type, f.id AS foo_id
   FROM  (
      VALUES
         (text 'testing', text 'blue')
       , ('another row', 'red'   )
       , ('new row1'   , 'purple')
       , ('new row2'   , 'purple')
      ) val (description, type)
   LEFT   JOIN foo f USING (type)
   )
, ins AS ( 
   INSERT INTO foo (type)
   SELECT DISTINCT type FROM sel WHERE foo_id IS NULL
   RETURNING id AS foo_id, type
   )
INSERT INTO bar (description, foo_id)
SELECT sel.description, COALESCE(sel.foo_id, ins.foo_id)
FROM   sel
LEFT   JOIN ins USING (type);

注意要插入的两个新虚拟行。两者均为紫色,尚不存在foo行说明了需要DISTINCT在第一个INSERT声明。

分步说明

  1. 第一个CTE sel提供多行输入数据。val带有VALUES表达式的子查询可以替换为表或子查询作为源。马上LEFT JOINfoo到追加foo_id的预先存在的type行。所有其他行都采用foo_id IS NULL这种方式。

  2. 第2个CTE ins不同的新类型(foo_id IS NULL)插入foo,并返回新生成的foo_id-和一起type返回以插入行。

  3. 现在,最终的外部对象INSERT可以为每行插入一个foo.id:预先存在的类型,或者它是在步骤2中插入的。

严格地说,这两个刀片发生“并联”,但由于这是一个单独的声明中,默认FOREIGN KEY的限制不会抱怨。默认情况下,引用完整性在语句的末尾强制执行。

适用于Postgres 9.3的SQL Fiddle(与9.1相同。)

如果您同时运行多个这些查询,则竞争条件很小。在此处此处此处阅读有关问题的更多信息。确实只有在繁重的并发负载下才会发生。与像在另一个答案中宣传的那样缓存解决方案相比,机会微不足道

重复使用功能

为了重复使用,我将创建一个SQL函数,该函数将记录数组作为参数并unnest(param)代替VALUES表达式使用。

或者,如果记录数组的语法对您来说太麻烦了,请使用逗号分隔的字符串作为parameter _param。例如形式:

'description1,type1;description2,type2;description3,type3'

然后使用它替换VALUES上面的语句中的表达式:

SELECT split_part(x, ',', 1) AS description
       split_part(x, ',', 2) AS type
FROM unnest(string_to_array(_param, ';')) x;


在Postgres 9.5中具有UPSERT的功能

创建用于参数传递的自定义行类型。我们可以不用它,但是更简单:

CREATE TYPE foobar AS (description text, type text);

功能:

CREATE OR REPLACE FUNCTION f_insert_foobar(VARIADIC _val foobar[])
  RETURNS void AS
$func$
   WITH val AS (SELECT * FROM unnest(_val))    -- well-known row type
   ,    ins AS ( 
      INSERT INTO foo AS f (type)
      SELECT DISTINCT v.type                   -- DISTINCT!
      FROM   val v
      ON     CONFLICT(type) DO UPDATE          -- type already exists
      SET    type = excluded.type WHERE FALSE  -- never executed, but lock rows
      RETURNING f.type, f.id
      )
   INSERT INTO bar AS b (description, foo_id)
   SELECT v.description, COALESCE(f.id, i.id)  -- assuming most types pre-exist
   FROM        val v
   LEFT   JOIN foo f USING (type)              -- already existed
   LEFT   JOIN ins i USING (type)              -- newly inserted
   ON     CONFLICT (description) DO UPDATE     -- description already exists
   SET    foo_id = excluded.foo_id             -- real UPSERT this time
   WHERE  b.foo_id IS DISTINCT FROM excluded.foo_id  -- only if actually changed
$func$  LANGUAGE sql;

呼叫:

SELECT f_insert_foobar(
     '(testing,blue)'
   , '(another row,red)'
   , '(new row1,purple)'
   , '(new row2,purple)'
   , '("with,comma",green)'  -- added to demonstrate row syntax
   );

快速且坚如磐石,适用于具有并发事务的环境。

除了上面的查询之外,这...

  • ...适用SELECTINSERTfoo:任何type不中FK表中存在,但是,插入。假设大多数类型已经存在。为了绝对确定并排除竞争条件,我们需要的现有行被锁定(以便并发事务不会干扰)。如果这对于您的情况来说太偏执了,您可以替换:

      ON     CONFLICT(type) DO UPDATE          -- type already exists
      SET    type = excluded.type WHERE FALSE  -- never executed, but lock rows

      ON     CONFLICT(type) DO NOTHING
  • ...适用于INSERTUPDATE(true“ UPSERT”)应用于bar:如果description已经存在,type则更新:

      ON     CONFLICT (description) DO UPDATE     -- description already exists
      SET    foo_id = excluded.foo_id             -- real UPSERT this time
      WHERE  b.foo_id IS DISTINCT FROM excluded.foo_id  -- only if actually changed

    但仅当type实际更改时:

  • ...通过VARIADIC参数传递值以及众所周知的行类型。注意默认的最大100个参数!相比:

    还有许多其他方法可以传递多行...

有关:


在您的INSERT missing FK rows at the same time示例中,将其放入事务中是否可以降低SQL Server中竞争条件的风险?
element11

1
@ element11:答案是针对Postgres的,但是由于我们在谈论单个 SQL命令,因此无论如何它都是单个事务。在更大的事务中执行它只会增加可能出现竞争状况的时间窗口。对于SQL Server:完全不支持数据修改CTE(仅SELECTWITH子句中)。资料来源:MS文档。
Erwin Brandstetter

1
您也可以使用INSERT ... RETURNING \gsetin 进行此操作,psql然后将返回值用作psql :'variables',但这仅适用于单行插入。
Craig Ringer

@ErwinBrandstetter很棒,但是我对sql来说还太陌生,无法理解所有内容,可以在“同时插入缺少的FK行”中添加一些注释来解释它的工作原理吗?另外,感谢SQLFiddle的工作示例!
glallen

@glallen:我添加了分步说明。也有许多指向相关答案和手册的链接,其中包含更多说明。您需要了解查询的作用,否则您可能会头疼。
Erwin Brandstetter

4

抬头。您基本上需要foo id将其插入bar。

顺便说一句,不是特定于Postgres。(并且您没有这样标记它)-这通常是SQL的工作方式。这里没有捷径。

但是,在应用程序方面,您可能在内存中缓存了foo项。我的表通常最多包含3个唯一字段:

  • 表级主键的ID(整数或其他值)。
  • 标识符,它是一个GUID,可作为稳定的ID应用程序级别使用(并且可能在URL等中暴露给客户)
  • 代码-可能存在的字符串,如果存在则必须是唯一的(SQL Server:经过过滤的唯一索引不为null)。那是客户集标识符。

例:

  • 帐户(在交易应用程序中)-> ID是用于外键的整数。->标识符是Guid,并在Web门户等中使用。-始终被接受。->代码是手动设置的。规则:设置后不会改变。

显然,当您要将某些内容链接到帐户时-首先,从技术上来说,您必须获得ID-但由于Identifier和Code都永远不会更改,因此,内存中的正向缓存会阻止大多数查询访问数据库。


10
您知道可以让RDBMS在单个SQL语句中为您执行查找,从而避免容易出错的缓存吗?
Erwin Brandstetter

您知道查找不变的元素不容易出错吗?同样,由于许可成本的缘故,通常,RDBMS不可扩展,并且是游戏中最昂贵的元素。从中获得尽可能多的负载并不是一件坏事。同样,很少有ORM支持它。
TomTom

14
不变的元素?最贵的元素?许可费用(对于PostgreSQL)?定义什么理智的ORM?不,我不知道所有这些。
Erwin Brandstetter
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.