使用 Ruby on Rails 和 PostgreSQL 的 Big IN 列表带来的大问题

主要观点:

  • 介绍了在关系数据库和 ORM 中常见的性能问题,重点关注“Big INs”这种有大量值的 IN 子句查询模式。
  • 探讨了该模式的起源、性能不佳的原因,以及在项目中可使用的替代方法。

关键信息和重要细节:

  • IN 子句与大量值:技术上称为“parenthesized list of scalar expressions”,如WHERE author_id IN (1,2,3),目的是进行过滤,在 Postgres 执行计划中显示为Filter: (author_id = ANY ('{1,2,3}'::integer[]))
  • 解析、规划与执行:大量值被视为常量,无相关统计信息,导致查询解析和内存使用增加,规划器易误估基数和行选择性,选择顺序扫描而非索引扫描,造成性能下降。
  • 直接创建模式:在 Active Record 中,通过pluck(:id)收集 id 列表,再将其作为参数传递给另一个查询,如author_ids = Author.where("created_at >=?", 1.year.ago).pluck(:id),然后Book.where(author_id: author_ids)
  • Active Record ORM 方法创建模式:使用includes()preload()等 eager loading 方法会产生IN子句 SQL 查询,如Book.includes(:author).limit(10)会产生两条查询,第二条有IN子句。
  • 修复 N+1 问题:通过添加includes(:author)消除 N+1 查询,但会产生有IN子句的第二条查询,性能可能受影响。
  • 使用 eager_load 进行 eager 加载eager_load产生单个使用LEFT OUTER JOIN的 SQL 查询,无IN子句,利用了两表的统计信息,更易让规划器正确估计选择性和基数。
  • 替代方法使用 ANY 或 SOMEANYSOMEIN更灵活,如使用 CTE 的WITH author_ids AS (SELECT id FROM authors) SELECT title FROM books WHERE author_id = ANY (SELECT id FROM author_ids),可使用ActiveRecordExtendedany方法在 Active Record 中生成类似查询。
  • 使用 VALUES 子句:如WITH ids(author_id) AS (VALUES(1),(2),(3)) SELECT title FROM books JOIN ids USING (author_id)SELECT title FROM books WHERE author_id IN (SELECT id FROM (VALUES(1),(2),(3)) AS v(id))VALUES子句被视为关系,有助于连接策略选择。
  • 使用临时表存储 id:创建临时表CREATE TEMP TABLE temp_ids (author_id int); INSERT INTO temp_ids(author_id) VALUES (1),(2),(3); CREATE INDEX ON temp_ids(author_id); SELECT title FROM books b JOIN temp_ids t ON t.author_id = b.author_id;
  • 使用 ANY 和数组值:如SELECT title FROM books WHERE author_id = ANY (ARRAY[1, 2, 3])ANY形式性能更好,支持预处理语句。
  • 测试替代查询结构:建议在生产样数据上测试,在 SQL 层工作,使用EXPLAIN (ANALYZE, BUFFERS)收集和分析查询执行计划,将 SQL 转换回 Active Record 源代码。
  • 在 pg_stat_statements 中查找 IN 子句查询:通过查询pg_stat_statementsquery字段查找包含问题IN查询的统计信息,但可能有重复或近似重复,需筛选'%IN \(%'
  • Postgres 17 中的改进:2024 年 Postgres 17 版本改进了处理标量表达式和索引的效率,减少重复扫描,提高执行速度,无需更改 SQL 查询或 ORM 代码。
  • Postgres 18 中的改进:2025 年 Postgres 18 版本有相关改进,如将x IN (VALUES...)自动转换为 ScalarArrayOpExpr,“Squash query list jumbling”以解决pg_stat_statements中因参数数量导致的查询条目拆分问题。

结论:

  • 了解了“Big INs”模式的问题和替代方法,可将其转换为连接查询,或使用ANY运算符、VALUES子句和预处理语句,以优化数据库性能。
阅读 49
0 条评论