使用复杂的双连接来获取子对象的计数

时间:2013-02-13 00:13:22

标签: sql postgresql join

请注意,我正在使用postgresql

我有一个organizations表,一个users表,一个jobs表和一个documents表。我想获得一份按订购的组织列表,这些组织可以访问他们。

organizations
------------
id (pk)
company_name

users
------------
id (pk)
organization_id

jobs
------------
id (pk)
client_id    (id of an organization)
server_id    (id of an organization)
creator_id   (id of a user)

documents
------------
id (pk)
job_id

所需结果

organizations.id  |  organizations.company_name  |  document_count
85                |  Big Corporation             |  84
905               |  Some other folks            |  65
403               |  ACME, Inc                   |  14

如您所见,组织可以通过3种不同的路径连接到文档:

  1. organizations.id => jobs.client_id => documents.job_id
  2. organizations.id => jobs.server_id => documents.job_id
  3. organizations.id => users.organization_id => jobs.creator_id => documents.job_id
  4. 但是我想要一个查询,它将获得每个公司有权访问的所有文件的数量......

    我尝试了几件事......就像这样:

    SELECT COUNT(documents.id) document_count, organizations.id, organizations.company_name
    FROM organizations
    INNER JOIN users ON organizations.id = users.organization_id
    INNER JOIN jobs ON (
      jobs.client_id = organizations.id OR
      jobs.server_id = organizations.id OR
      jobs.creator_id = users.id
    )
    INNER JOIN documents ON documents.job_id = jobs.id
    GROUP BY organizations.id, organizations.company_name
    ORDER BY document_count DESC
    LIMIT 10
    

    查询需要一段时间才能运行,但这并不可怕,因为我正在为一次性报告执行此操作,但结果......可能不正确。

    第一个列出的组织报告的文件数量为129,834个 - 但这是不可能的,因为documents表中只有32,820个记录。我觉得它必须计算大量的重复项(由于我的一个连接中的错误?)但我不确定我哪里出错了。

    订单显示正确,因为系统的最高容量用户显然位于列表的顶部...但是值以某种方式膨胀。

5 个答案:

答案 0 :(得分:1)

问题在于,如果jobs.client_id = organizations.idjobs.server_id = organizations.id,则无法过滤您的INNER JOIN users(除了ON条款),所以你&# 39;将为属于该组织的每个用户获取单独的记录。换句话说,对于每个组织,您都要添加三个值:

  • 其用户总数属于其客户的作业的文档总数
  • 其用户总数属于其服务器的作业的文档总数
  • 属于作业的文档总数,如果其用户是创建者,则为

解决此问题的一种方法是删除INNER JOIN users行,然后更改:

  jobs.creator_id = users.id

到此:

  jobs.creator_id IN (SELECT id FROM users WHERE organization_id = organizations.id)

。 。 。但这可能表现得非常糟糕。在找到可接受的查询之前,您可能需要尝试一些事情。

答案 1 :(得分:1)

简化你的想法。你有3个docid路径,所以写3个查询,联合它们并计算

答案 2 :(得分:0)

重新设计它可能为时已晚,但你真的应该这样做。

jobs表不应该有自己的id字段和密钥。

作业表设计非常糟糕,因为每次从id索引对磁盘页面的引用都必须从数据文件中读取1-100个不同的页面才能获得总是想要使用(这是一个工作不应该有自己的id的线索)。

您可以通过使作业使用作业ID字段上的群集或群集索引(取决于数据库系统)来快速解决问题。另一种方法是将其他三个id字段标记为索引上的“包含”,这样页面读取到数据文件就会100%消失。这些中的任何一个都足以使这个“正常工作”。

我鼓励你做的是将id字段和键放在作业上,而是创建一个“自然键”,其中包含其他三个id字段,并在文档表中使用该键。

我还会在作业表和文档表上士气(重复)创建者的组织。用户不会移动到另一个组织并保持相同的访问权限,因此您永远不必运行扫描来同步更新这些访问,即使您这样做也很容易。

通过这些更改,您可以直接在文档表上执行选择,跳过其他表中所需的随机页面读取。在三个不同的id字段中分组的组将有点棘手。我可能会尝试这个,因为它很有趣。

在短期内,尝试群集或包含在作业表上以解决性能问题,我将在今晚检查连接逻辑。

答案 3 :(得分:0)

但是我想要一个查询来获取您有权访问的所有文件的数量......

这就是你的查询开始的地方:

SELECT ... FROM documents
...

由于文档表的唯一线索是作业,因此您还需要作业表::

SELECT ... 
FROM documents dc
JOIN jobs jo ON jo.document_id = dc.id
...

现在,是时候进行限制了。 您真正想要哪些文件?您需要三种情况:client_id匹配 组织,或者server_id maches 公司,或者creator_id匹配恰好为工作的用户公司:

SELECT ... 
FROM documents dc
JOIN jobs jo ON jo.document_id = dc.id
WHERE jo.client_id = $THE_COMPANY
   OR jo.server_id = $THE_COMPANY
   OR EXISTS (
      SELECT *
      FROM users uu
      JOIN organizations oo ON uu.organization_id = ex.id
      WHERE uu.id = jo.creator_id
        AND oo.id = $THE_COMAPNY
      )
     ;

但是,这里可能存在问题。如果两个或多个不同的作业记录指向同一个文档,那么您将计算这两个。您可以向外部查询添加DISTINCT,也可以将jobs-table向下移动到子查询中:

SELECT ... 
FROM documents dc
WHERE EXISTS (
  SELECT *
  FROM jobs jo
  WHERE jo.document_id = dc.id
  AND ( jo.client_id = $THE_COMPANY
      OR jo.server_id = $THE_COMPANY
      OR EXISTS (
        SELECT *
        FROM users uu
        JOIN organizations oo ON uu.organization_id = ex.id
        WHERE uu.id = jo.creator_id
        AND oo.id = $THE_COMAPNY
        )
      )
    )
  ;

正如您所看到的,选择文档的方式最终会出现在WHERE (a OR b OR c)条款中。

更新:(由于OP没有以可用的形式向我们提供表格定义,我必须重建这些表格)

DROP SCHEMA tmp CASCADE;
CREATE SCHEMA tmp ;
SET search_path=tmp;
        --
        -- create the missing tables
        --
CREATE TABLE organizations
        ( id SERIAL NOT NULL PRIMARY KEY
        , company_name varchar
        );

CREATE TABLE users
        ( id SERIAL NOT NULL PRIMARY KEY
        , organization_id INTEGER NOT NULL REFERENCES organizations(id)
        );

CREATE TABLE jobs
        ( id SERIAL NOT NULL PRIMARY KEY
        , client_id    INTEGER NOT NULL REFERENCES organizations(id)
        , server_id    INTEGER NOT NULL REFERENCES organizations(id)
        , creator_id   INTEGER NOT NULL REFERENCES users(id)
        );

CREATE TABLE documents
        ( id SERIAL NOT NULL PRIMARY KEY
        , job_id INTEGER NOT NULL REFERENCES jobs(id)
        );
        --
        -- Populate
        --
INSERT INTO organizations(id, company_name) VALUES
 (85,'Big Corporation') ,(905,'Some other folks') ,(403,'ACME, Inc')
        ;
select setval('organizations_id_seq', 905);

INSERT INTO users(organization_id)
SELECT o.id
FROM generate_series(1,1000)
JOIN organizations o ON random() < 0.3
        ;
INSERT INTO jobs (client_id,server_id,creator_id)
SELECT o1.id, o2.id, u.id
FROM users u
JOIN organizations o1 ON 1=1
JOIN organizations o2 ON o2.id <> o1.id
        ;
INSERT INTO documents(job_id)
SELECT id FROM jobs j
        ;
DELETE FROM documents
WHERE random() < 0.5
        ;

        --
        -- And the query ...
        --
EXPLAIN ANALYZE
SELECT o.id AS org
        , count(*) AS the_docs
FROM organizations o
JOIN documents d  ON 1=1 -- start with a carthesian product
WHERE EXISTS (
        SELECT *
        FROM jobs j
        WHERE d.job_id = j.id
        AND (j.client_id = o.id OR j.server_id = o.id )
        )
OR EXISTS (
        SELECT *
        FROM jobs j
        JOIN users u ON j.creator_id = u.id
        WHERE u.organization_id = o.id
        AND d.job_id = j.id
        )
GROUP BY o.id
        ;

答案 4 :(得分:0)

除了建议UNION之外,没有一个答案能让我在那里。这就是我想出的:

SELECT COUNT(docs.doc_id) document_count, docs.org_id, docs.org_name
FROM (
  SELECT documents.id doc_id, organizations.id org_id, organizations.company_name org_name
  FROM documents
  INNER JOIN jobs ON documents.job_id = jobs.id
  INNER JOIN organizations ON jobs.client_id = organizations.id
  UNION
  SELECT documents.id doc_id, organizations.id org_id, organizations.company_name org_name
  FROM documents
  INNER JOIN jobs ON documents.job_id = jobs.id
  INNER JOIN organizations ON jobs.server_id = organizations.id
  UNION
  SELECT documents.id doc_id, organizations.id org_id, organizations.company_name org_name
  FROM documents
  INNER JOIN jobs on documents.job_id = jobs.id
  INNER JOIN users ON jobs.creator_id = users.id
  INNER JOIN organizations ON users.organization_id = organizations.id
) docs
GROUP BY org_id, org_name
ORDER BY document_count DESC

性能比任何建议子查询的人要好得多,而且它似乎给了我一个合理的答案