为JSON数组数据加入而不是子查询?

时间:2017-11-14 07:38:01

标签: sql json query-performance postgresql-9.6

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

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

你知道,一个人可能是多家公司的联系人,即使是“对”(如'{1,2}'也可能是相同的)。

现在要求,在查询公司时:

  • 每家公司一排
  • 联系人的详细信息应作为 JSON数组(例如[{"id":1,"person_name":"John"}]
  • )一行返回

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

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;

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

  • 使用JOIN会使查询更快吗?如果是:我将如何使JOIN返回JSON数组?
  • 使用索引会改善性能吗?如果是:哪一列哪个索引?

更新

仅供参考,我想指出RadimBač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 ($1,$2)',[i,'SomePerson' + i]);
 }

for(var i = 1; i < 10000; i++) {
   plv8.execute('INSERT INTO test_company (id, company_name, contact_person) VALUES ($1,$2,$3)',[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;

相比,这给了我(总是在本地机器上用pgAdmin 3测量)大约23秒的执行时间
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秒

BTW:我曾经听说子查询test_company.contact_person @> ARRAY[id]id = ANY(test_company.contact_person)快。据我测试,这不是真的。在我的例子中,后一个版本在23秒内返回所有行,而第一个版本花了46秒。

1 个答案:

答案 0 :(得分:1)

我会使用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

使用主键自动创建索引,因此性能应该没问题。问题是:你真的想要所有公司及其所有联系人吗?根本没有过滤器?

编辑根据您的评论,我至少会使用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");