JOIN 而不是 JSON 数组数据的子查询?

JOIN instead of subquery for JSON array data?

我有数据库设计(在 PostgreSQL 9.6 中),其中存储了一个 table 公司数据。每家公司可以有一个或多个联系人,其详细信息在另一个 table 中列出。 (简化的)模式是这样的:

DROP TABLE IF EXISTS test_company;
CREATE TABLE test_company (id integer, company_name text, contact_person integer[]);

DROP TABLE IF EXISTS test_contact_person;
CREATE TABLE test_contact_person (id integer, person_name text);

现在考虑这样的数据:

INSERT INTO test_company (id, company_name, contact_person) VALUES (1, 'Foo Ldt.', '{1,2}');
INSERT INTO test_company (id, company_name, contact_person) VALUES (2, 'Foo Sub Inc.', '{1,2}');
INSERT INTO test_company (id, company_name, contact_person) VALUES (3, 'Foo Sub Sub Inc.', '{1}');
INSERT INTO test_company (id, company_name, contact_person) VALUES (4, 'Bar Inc.', '{3,4}');
INSERT INTO test_company (id, company_name, contact_person) VALUES (5, 'Foo-Bar Joint-Venture', '{2,3,4}');

INSERT INTO test_contact_person(id, person_name) VALUES (1,'John');
INSERT INTO test_contact_person(id, person_name) VALUES (2,'Maria');
INSERT INTO test_contact_person(id, person_name) VALUES (3,'Bill');
INSERT INTO test_contact_person(id, person_name) VALUES (4,'Jane');

你看,一个人可能是多家公司的联系人,甚至"pairs"(像'{1,2}'也可能是一样的)。

现在查询公司时的要求是:

现在,我正在用这样的子查询解决这个问题:

SELECT
id,
company_name,
 (
  SELECT json_agg(my_subquery) FROM
   (
     SELECT id, person_name FROM test_contact_person
     WHERE id = ANY(test_company.contact_person)
   )
  AS my_subquery 
)
contact_person_expanded
FROM test_company;

这给了我预期的结果。然而(一如既往)表现并不令人满意。顺便说一句:目前 table 上都没有索引。我现在在想:

更新

仅供参考,我想指出 Radim Bača 建议的解决方案似乎在提高性能方面有效。

首先,我使用丑陋的 plv8 循环输入了更多数据

DROP TABLE IF EXISTS test_company;
CREATE TABLE test_company (id integer, company_name text, contact_person integer[]);

DROP TABLE IF EXISTS test_contact_person;
CREATE TABLE test_contact_person (id integer, person_name text);

DO $$
 for(var i = 1; i < 20000; i++) {
   plv8.execute('INSERT INTO test_contact_person(id, person_name) VALUES (,)',[i,'SomePerson' + i]);
 }

for(var i = 1; i < 10000; i++) {
   plv8.execute('INSERT INTO test_company (id, company_name, contact_person) VALUES (,,)',[i,'SomeCompany' + i,[i,(20 -i)]]);
 }
$$ LANGUAGE plv8;

然后我再次尝试了我的查询版本:

SELECT
id,
company_name,
 (
  SELECT json_agg(my_subquery) FROM
   (
     SELECT id, person_name FROM test_contact_person
     WHERE id = ANY(test_company.contact_person)
   )
  AS my_subquery 
)
contact_person_expanded
FROM test_company;

相比,这给了我大约 23 秒的执行时间(总是在我本地机器上的 pgAdmin 3 中测量)
SELECT
  comp.id,
  comp.company_name,
  json_agg(json_build_object('id', pers.id, 'person_name', pers.person_name)) AS contact_person_expanded
FROM test_company comp
JOIN test_contact_person pers ON comp.contact_person @> ARRAY[pers.id]
GROUP BY comp.id, comp.company_name

这大约需要 47 秒 - 没有索引。

最后,我加了一个索引:

DROP INDEX IF EXISTS idx_testcompany_contactperson;
CREATE INDEX idx_testcompany_contactperson on test_company USING GIN ("contact_person");

带有子查询的版本的执行时间没有改变,但是当使用 JOIN 时,效果是戏剧性的:1.1 秒!

顺便说一句:我曾经听说在子查询中 test_company.contact_person @> ARRAY[id]id = ANY(test_company.contact_person) 快。据我测试,事实并非如此。在我的例子中,后一个版本 return 在 23 秒内编辑了所有行,而第一个版本用了 46 秒。

我会为 M:N 基数

使用通用关系方法
CREATE TABLE company (cid integer primary key, company_name text);
CREATE TABLE contact_person (pid integer primary key, person_name text);
CREATE TABLE contact(
    cid integer references company,
    pid integer references contact_person,
    primary key(cid, pid)
);

对于第一人称,您只需添加以下值

INSERT INTO contact VALUES (1, 1);
INSERT INTO contact VALUES (1, 2);
-- and so on

如果您随后需要公司及其联系人,您只需使用以下 JOIN 和 JSON 聚合

SELECT c.cid, 
   c.company_name,
   json_agg(json_build_object('id', cp.pid, 'person_name', cp.person_name)) 
FROM company c
JOIN contact ct ON c.cid = ct.cid
JOIN contact_person cp ON cp.pid = ct.pid
GROUP BY c.cid, c.company_name

demo

索引是用主键自动创建的,所以性能应该没问题。问题是:您真的想要所有公司及其所有联系人吗?完全没有过滤器?

EDIT 根据您的评论,我至少会使用 JOIN 而不是相关子查询来重写您的查询。它可能有助于优化器找到更好的计划。

SELECT
  comp.id,
  comp.company_name,
  json_agg(json_build_object('id', pers.id, 'person_name', pers.person_name))
FROM test_company comp
JOIN test_contact_person pers ON comp.contact_person @> ARRAY[pers.id]
GROUP BY comp.id, comp.company_name

这个符号应该允许 Postgresql 使用这样的 GIN 索引

CREATE INDEX idx_testcompany_contactperson on test_company USING GIN ("contact_person");