用于 JOIN 查询的 Postgresql 分页性能

Posted

技术标签:

【中文标题】用于 JOIN 查询的 Postgresql 分页性能【英文标题】:Postgresql paging perfomance for JOIN query 【发布时间】:2020-08-16 09:39:29 【问题描述】:

我有两张桌子:

products ~ 2000 万条记录

tags ~3500 万条记录

并且对此类查询有性能问题:

SELECT products.name 
FROM products 
INNER JOIN tags ON tags.product_id=products.id
WHERE 
  products.categoryid IN (1,2,3..) AND
  tags.id = 3 
ORDER BY products.position 
LIMIT 64 OFFSET 0;

当 OFFSET 很小时,一切都很好。当 OFFSET 大约 >1000 时,查询执行 10-80 秒。

已经有ORDER BY position(产品表)和product_id, id(标签表)的索引。

    (OFFSET=100)
    Limit  (cost=85705.10..147411.97 rows=72 width=667) (actual time=1623.013..2334.542 rows=72 loops=1)
          ->  Nested Loop  (cost=1.13..67736996.34 rows=79036 width=667) (actual time=4.898..2334.493 rows=172 loops=1)
                ->  Index Scan using items_sorting_index on products  (cost=0.56..61905349.96 rows=1513209 width=667) (actual time=0.083..2188.800 rows=17547 loops=1)
                      Filter: (categoryid = ANY ('1,2,3,4,5,6,7,8,9,11,12,13,14,15,16,17,18,19,20'::bigint[]))
                      Rows Removed by Filter: 638658
                ->  Index Only Scan using index_tags_on_product_id_and_tag_id on tags  (cost=0.56..3.84 rows=1 width=4) (actual time=0.008..0.008 rows=0 loops=17547)
                      Index Cond: ((product_id = products.id) AND (id = 3))
                      Heap Fetches: 28
        Planning time: 1.345 ms
        Execution time: 2334.608 ms

tags.id在查询中可以不同,不同标签的结果元素也不同,所以我觉得不能用cursor-field这样的方法。

如何优化这个查询?

【问题讨论】:

您的统计数据看起来有点偏离(产品表中的预期rows=1513209 与实际rows=17547)。您可能想先在两个表上运行ANALYZE。您可以尝试的另一件事是让items_sorting_index 索引覆盖name 列(请参阅the docs)。 @Marth 统计信息已关闭,因为“预期”行没有考虑 LIMIT,而实际行则考虑了。 我不明白你对光标的反对意见。还是“光标字段”与光标不同? @jjanes 将字段添加到表中,这将有助于计算页面位置(如allyouneedisbackend.com/blog/2017/09/24/…)。这不是我的问题 键集分页应该可以正常工作,只要人们实际上是在逐步浏览数据,而不是想在没有先看到 1-49 的情况下跳到第 50 页。如果 position 不是唯一的,则需要在 ORDER BY 中添加一个 tie breaker。 【参考方案1】:

您可以在加入前通过过滤器标签表减少数据集,例如:

SELECT products.name 
FROM products 
INNER JOIN (
  SELECT id FROM tags WHERE tags.id = 3
) tags ON tags.product_id=products.id
WHERE 
  products.categoryid IN (1,2,3..)
ORDER BY products.position 
LIMIT 64 OFFSET 0;

或者分别过滤两个表并连接结果:

SELECT products.name 
FROM (
  SELECT name, position FROM products WHERE products.categoryid IN (1,2,3..)
) products 
INNER JOIN (
  SELECT id FROM tags WHERE tags.id = 3
) tags ON tags.product_id=products.id
ORDER BY products.position 
LIMIT 64 OFFSET 0;

【讨论】:

ЕслимыделаемJOINдвухтаблиц,записисравниваютсяпоследовательноизподходящихпокритериямилисоздаютсядвабольшихнабора,которыеобъединяютсяпоусловию? ВовторомвашемпримерееслиуменяБудетмиллионызаписейсtags.id= 3,ТонебудетЛиэтоТяжелодлябдсоздатьтакойdatasetдляпоследушего加入? span> Уважаемый @НиколайАгеев,请用英文写下您的评论,以便所有 SO 用户都能阅读和协作。关于我的建议,测试它的最佳方法是测试您的数据集。只需在此处运行查询和评论是否有帮助【参考方案2】:

您可以很好地使用键集分页。初始查询是:

SELECT products.name, products.position, products.id
FROM products 
INNER JOIN tags ON tags.product_id=products.id
WHERE 
  products.categoryid IN (1,2,3..) AND
  tags.id = 3 
ORDER BY products.position, products.id
LIMIT 64;

下面的查询将是

SELECT products.name, products.position, products.id
FROM products 
INNER JOIN tags ON tags.product_id=products.id
WHERE 
  products.categoryid IN (1,2,3..) AND
  tags.id = 3
  AND (products.position, products.id) > ($1, $2)
ORDER BY products.position, products.id
LIMIT 64;

其中$1$2 是前面查询返回的最后一个值。

products (products.position, products.id) 上的索引可以加快查询速度。

【讨论】:

如果位置是布尔字段怎么办?或者,如果我需要使用 products.stock(真/假)。结果不会按升序排序 什么boolean 字段?在你的问题中没有这样的东西。但是很容易在结果或ORDER BY 子句中添加更多列。

以上是关于用于 JOIN 查询的 Postgresql 分页性能的主要内容,如果未能解决你的问题,请参考以下文章

Postgresql中的Inner Join子查询导致错误

left join分页查询

有join的sql语句如何写分页查询?

PostgreSQL 查询到 JOIN 和 SUM

Postgresql分页Limit,如何将Mysql的分页转换为Postgresql的分页

Postgresql分页Limit,如何将Mysql的分页转换为Postgresql的分页