具有多个值的数组列上的 LEFT OUTER JOIN

LEFT OUTER JOIN on array column with multiple values

当一个 table 不是数组值,而另一个 table 的数组值时,我似乎找不到通过数组列连接两个 table 的技巧可以包含多个值。当有一个单值数组时它确实有效。

这是我所谈论内容的一个简单的最小示例。真正的 tables 在数组列 FWIW 上有 GIN 索引。这些没有,但查询行为相同。

DROP TABLE IF EXISTS eg_person;
CREATE TABLE eg_person (id INT PRIMARY KEY, name TEXT);
INSERT INTO eg_person (id, name) VALUES
  (1, 'alice')
, (2, 'bob')
, (3, 'charlie');

DROP TABLE IF EXISTS eg_assoc;
CREATE TABLE eg_assoc (aid INT PRIMARY KEY, actors INT[], benefactors INT[]);
INSERT INTO eg_assoc (aid, actors, benefactors) VALUES
  (1, '{1}'  , '{2}')
, (2, '{1,2}', '{3}')
, (3, '{1}'  , '{2,3}')
, (4, '{4}'  , '{1}');

SELECT aid, actors, a_person.name, benefactors, b_person.name 
FROM   eg_assoc
LEFT   JOIN eg_person a_person on array[a_person.id] @> eg_assoc.actors
LEFT   JOIN eg_person b_person on array[b_person.id] @> eg_assoc.benefactors;

实际结果是这样的。这里的问题是,如果 actorsbenefactors 包含多个值,名称列会出现 NULL

 aid | actors | name  | benefactors |  name   
-----+--------+-------+-------------+---------
   1 | {1}    | alice | {2}         | bob
   2 | {1,2}  |       | {3}         | charlie
   3 | {1}    | alice | {2,3}       | 
   4 | {4}    |       | {1}         | alice

我期待的是:

 aid | actors | name  | benefactors |  name   
-----+--------+-------+-------------+---------
   1 | {1}    | alice | {2}         | bob
   2 | {1,2}  | alice | {3}         | charlie
   2 | {1,2}  | bob   | {3}         | charlie
   3 | {1}    | alice | {2,3}       | bob
   3 | {1}    | alice | {2,3}       | charlie
   4 | {4}    |       | {1}         | alice

不过如果我能把它变成这样就好了:

 aid | actors | name        | benefactors |  name   
-----+--------+-------------+-------------+---------
   1 | {1}    | {alice}     | {2}         | {bob}
   2 | {1,2}  | {alice,bob} | {3}         | {charlie}
   3 | {1}    | {alice}     | {2,3}       | {bob, charlie}
   4 | {4}    |             | {1}         | {alice}

我知道此模式已非规范化,如果需要,我愿意使用常规表示。然而,这是一个汇总查询,它已经涉及比我想要的更多的连接。

您需要使用 = ANY() 运算符:

SELECT aid, actors, a_person.name, benefactors, b_person.name 
FROM eg_assoc
  LEFT JOIN eg_person a_person on a_person.id = any(eg_assoc.actors)
  LEFT JOIN eg_person b_person on b_person.id = any(eg_assoc.benefactors);

It would be really nice if I could get it to look like this though.

只需根据 aid:

汇总值
SELECT aid, min(actors), array_agg(distinct a_person.name), min(benefactors), array_agg(distinct b_person.name)
FROM   eg_assoc
  LEFT JOIN eg_person a_person on a_person.id = any(eg_assoc.actors)
  LEFT JOIN eg_person b_person on b_person.id = any(eg_assoc.benefactors)
group by aid;

是的,overlap operator && can use a GIN index on arrays。对于像这样的查询非常有用,可以在一组演员中查找给定人员 (1) 的行:

SELECT * FROM eg_assoc WHERE actors && '{1}'::int[]

但是,您的查询逻辑是相反的,查找eg_assoc中数组中列出的所有人。 GIN 索引 no 在这里帮助。我们只需要 PK 的 btree 索引 person.id.

正确查询

基础知识:

  • PostgreSQL unnest() with element number

以下查询保留原始数组与给定的完全一样,包括可能的重复元素和元素的原始顺序。适用于 一维数组 。额外的维度被折叠成一个单一的维度。保留多个维度更复杂(但完全可能):

WITH ORDINALITY 在 Postgres 9.4 或更高版本中

SELECT aid, actors
     , ARRAY(SELECT name
             FROM   unnest(e.actors) WITH ORDINALITY a(id, i)
             JOIN   eg_person p USING (id)
             ORDER  BY a.i) AS act_names
     , benefactors
     , ARRAY(SELECT name
             FROM   unnest(e.benefactors) WITH ORDINALITY b(id, i)
             JOIN   eg_person USING (id)
             ORDER  BY b.i) AS ben_names
FROM   eg_assoc e;

LATERAL 查询

对于 PostgreSQL 9.3+.

SELECT e.aid, e.actors, a.act_names, e.benefactors, b.ben_names
FROM   eg_assoc e
, LATERAL (
   SELECT ARRAY( SELECT name
                 FROM   generate_subscripts(e.actors, 1) i
                 JOIN   eg_person p ON p.id = e.actors[i]
                 ORDER  BY i)
   ) a(act_names)
, LATERAL (
   SELECT ARRAY( SELECT name
                 FROM   generate_subscripts(e.benefactors, 1) i
                 JOIN   eg_person p ON p.id = e.benefactors[i]
                 ORDER  BY i)
   ) b(ben_names);

db<>fiddle here 有几个变体。
sqlfiddle

微妙的细节:如果找不到某个人,它就会被丢弃。如果在整个数组中找不到任何人,这两个查询都会生成一个 空数组 ('{}')。其他查询样式将 return NULL。我在 fiddle.

中添加了变体

相关子查询

对于 Postgres 8.4+(其中引入了 generate_subsrcipts()):

SELECT aid, actors
     , ARRAY(SELECT name
             FROM   generate_subscripts(e.actors, 1) i
             JOIN   eg_person p ON p.id = e.actors[i]
             ORDER  BY i) AS act_names
     , benefactors
     , ARRAY(SELECT name
             FROM   generate_subscripts(e.benefactors, 1) i
             JOIN   eg_person p ON p.id = e.benefactors[i]
             ORDER  BY i) AS ben_names
FROM   eg_assoc e;

可能仍然表现最佳,即使在 Postgres 9.3 中也是如此。
ARRAY constructorarray_agg() 快。参见:

您查询失败

似乎可以完成这项工作,但它不可靠、具有误导性、可能不正确且不必要地昂贵。

  1. 代理交叉连接,因为两个不相关的连接。偷偷摸摸的反模式。参见:

    • Two SQL LEFT JOINS produce incorrect result

    array_agg() 中用 DISTINCT 进行了表面修复,以消除生成的重复项,但这实际上是在给猪涂口红。它还消除了原件中的重复项,因为此时无法区分差异——这可能是不正确的。

  2. 表达式a_person.id = any(eg_assoc.actors)有效,但从结果中消除了重复项(在此查询),除非指定,否则这是错误的。

  3. 数组元素的原始顺序未保留。这通常很棘手。但这在这个查询中变得更加严重,因为演员和捐助者被成倍增加并再次区分,保证任意顺序。

  4. 外部 SELECT 中没有列别名导致列名重复,这使得一些客户端失败(在没有别名的 fiddle 中无法工作)。

  5. min(actors)min(benefactors) 没用。通常人们只会将列添加到 GROUP BY 而不是假聚合它们。但是 eg_assoc.aid 无论如何都是 PK 列(覆盖 GROUP BY 中的整个 table),所以甚至没有必要。就 actors, benefactors.

聚合整个结果本来就是浪费时间和精力。使用不乘以基行的更智能的查询,那么您就不必将它们聚合回去。