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

时间:2015-02-18 17:19:22

标签: sql arrays postgresql left-join

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

这是我所谈论的一个简单的例子。实数表在数组列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;

实际结果是这样的。这里的问题是,如果NULLactors包含多个值,则名称列会显示benefactors

 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}

我知道这个架构是非规范化的,如果需要,我愿意去正常的表示。但是,这是一个摘要查询,它已经涉及比我更喜欢的更多连接。

2 个答案:

答案 0 :(得分:3)

&&运算符和索引?

你是对的overlap operator && could use a GIN index on arrays。对于以下查询非常有用(在actor中查找人1的行):

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

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

正确查询

这不是一个小问题。首先阅读:

以下查询保留原始数组完全按照给定,包括可能的重复元素和元素的原始顺序。适用于 1维数组。其他尺寸折叠成单个尺寸。保留多个维度(但完全可能)更复杂:

相关子查询

对于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;

即使在第9.3页,也可能表现最佳 使用比array_agg()更快的array constructor

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

SQL Fiddle有几个变体(第9.4版的版本除外)。

细微的细节:如果找不到人,就会被遗漏。如果没有找到整个阵列的人,这两个查询都会生成空数组 '{}' )。其他查询样式将返回 NULL 。我为小提琴添加了变种。

Postgres 9.4 + 中的

WITH ORDINALITY
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;

查询失败

query provided by @a_horse 似乎来完成这项工作,但它不可靠,误导,可能不正确且不必要的昂贵。

  1. 由于两个不相关的连接,代理交叉连接。一种偷偷摸摸的反模式。详细说明:

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

  2. 表达式a_person.id = any(eg_assoc.actors) 正常工作,但从结果中消除了重复(在此查询中发生了两次),除非另有说明,否则这是错误的。

  3. 不保留原始数组元素的顺序。一般来说这很棘手。但是在这个查询中它更加恶化,因为演员和恩人会成倍增加并再次变得明显,保证任意顺序。

  4. 外部SELECT中没有列别名会导致重复的列名称,这会使某些客户端失败(不在没有别名的小提琴中工作)。

  5. min(actors)min(benefactors)没用。通常只需将列添加到GROUP BY而不是伪造聚合它们。但eg_assoc.aid无论如何都是PK列(涵盖GROUP BY中的整个表格),因此甚至不需要。只需actors, benefactors

  6. 汇总整个结果是浪费时间和精力开始的。使用不会使基本行相乘的更智能的查询,然后您就不必将它们聚合回来。

答案 1 :(得分:1)

您需要使用= 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);
  

如果我能让它看起来像这样,那将是非常好的。

根据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;