加入联结表以实现高效排序/分页的推荐方法是什么?

Jef*_*era 6 postgresql performance join paging postgresql-performance

简介: 我有一个简单的数据库模式,但即使只有几十条记录,基本查询的性能也已经成为一个问题。

数据库:PostgreSQL 9.6

简化架构

CREATE TABLE article (
  id bigint PRIMARY KEY,
  title text NOT NULL,
  score int NOT NULL
);
CREATE TABLE tag (
  id bigint PRIMARY KEY,
  name text NOT NULL
);
CREATE TABLE article_tag (
  article_id bigint NOT NULL REFERENCES article (id),
  tag_id bigint NOT NULL REFERENCES tag (id),
  PRIMARY KEY (article_id, tag_id)
);
CREATE INDEX ON article (score);
Run Code Online (Sandbox Code Playgroud)

生产数据信息

所有表都是读/写的。写入量低,每几分钟左右只有一个新记录。

大概记录数:

  • ~66K 篇文章
  • ~63K 标签
  • ~147K article_tags

每篇文章平均 5 个标签。

问题:我想创建一个视图article_tags,其中包含每个文章记录的标签数组,可以按顺序排序article.score和分页,也可以不加过滤。

在我的第一次尝试中,我惊讶地发现查询花费了大约 350 毫秒来执行并且没有使用索引。在随后的尝试中,我能够将其降低到约 5 毫秒,但我不明白发生了什么。我希望所有这些查询花费相同的时间。我在这里缺少什么关键概念?

尝试(SQL 小提琴):

  1. 多表连接(~350 毫秒),(~5 毫秒,如果按 article.id 排序!)——似乎是最自然的解决方案
  2. subquery join (~300 ms) -- 似乎也是一个自然的解决方案
  3. 有限的子查询连接(~5 ms)——超级尴尬,不能用于查看
  4. 横向连接(~5 毫秒)——这真的是我应该使用的吗?似乎是对横向的误用
  5. ……还有什么?

Erw*_*ter 6

分页

对于分页LIMIT(和OFFSET) 是简单的,但对于更大的表格来说通常效率低下的工具。您的测试LIMIT 10只显示了冰山一角。无论您选择哪个查询,性能都会随着 的增长OFFSET降低

如果您没有或只有很少的并发写入访问权限,则更好的解决方案是MATERIALIZED VIEW添加行号,并在其上加上索引。您的所有查询都按行号选择行。

在并发写入负载下,这样的 MV 很快就会过时(但CONCURRENTLY每 N 分钟刷新一次MV 之类的妥协可能是可以接受的)。
LIMIT/OFFSET根本无法正常工作,因为“下一页”是那里的移动目标,并且LIMIT/OFFSET无法应对。最好的技术取决于未公开的信息。

有关的:

指数

您的索引通常看起来不错。但是您的评论表明该表tag很多行。通常,像 那样的表上的写负载很少tag,这非常适合仅索引支持。所以添加一个多列(“覆盖”)索引:

CREATE INDEX ON tag(id, name);
Run Code Online (Sandbox Code Playgroud)

有关的:

仅前 N 行

如果您实际上不需要更多页面(严格来说这不是“分页”),那么任何查询样式都可以减少从相关表中检索详细信息article 之前的合格行(昂贵)。您的“有限子查询”(3.)和“横向连接”(4.)解决方案很好。但你可以做得更好:

为变体使用ARRAY构造函数LATERAL

SELECT a.id, a.title, a.score, tags.names
FROM   article a
LEFT   JOIN LATERAL (
   SELECT ARRAY (
      SELECT t.name
      FROM   article_tag a_t 
      JOIN   tag t ON t.id = a_t.tag_id
      WHERE  a_t.article_id = a.id
   -- ORDER  BY t.id  -- optionally sort array elements
      )
  ) AS tags(names) ON true
ORDER  BY a.score DESC
LIMIT  10;
Run Code Online (Sandbox Code Playgroud)

LATERAL一个子查询组装标签 article_id的时间,所以GROUP BY article_id是多余的,以及连接条件ON tags.article_id = article.id和基本ARRAY构造是比便宜的array_agg(tag.name)为剩余的简单情况。

或者使用低相关的子查询,通常更快,但:

SELECT a.id, a.title, a.score
     , ARRAY (
         SELECT t.name
         FROM   article_tag a_t 
         JOIN   tag t ON t.id = a_t.tag_id
         WHERE  a_t.article_id = a.id
      -- ORDER  BY t.id  -- optionally sort array elements
      ) AS names
FROM   article a
ORDER  BY a.score DESC
LIMIT  10;
Run Code Online (Sandbox Code Playgroud)

db<>fiddle here
SQL Fiddle