【问题标题】:Flask-SqlAlchemy query with order_by and pagination is very slow使用 order_by 和分页的 Flask-SqlAlchemy 查询非常慢
【发布时间】:2016-07-29 21:30:36
【问题描述】:

我使用 Python=2.7.3、PostgreSQL=9.3、Flask=0.10.0、SQLAlchemy =1.10.12,Flask-SQLAlchemy=2.1 和 psycopg2=2.5.4

我在 PostgreSQL 中有一个 10 亿条记录的表,我必须对其进行分页并在其中提供搜索:

class MyTable(db.Model):
    """ My Table """
    id = db.Column(db.Integer, primary_key=True)
    code = db.Column(db.String(100), index=True, unique=True)
    name = db.Column(db.String(512), index=True)
    __tablename__ = 'my_table'

所以我在请求数据的代码中做了以下操作:

records = MyTable.query.filter(**filter_list).\
    order_by(asc('code')).paginate(page, per_page, False)

关键是如果 per_page=10 和 page=1158960 即使根本没有过滤,只需为最后一页选择 10 条最后记录需要大约 13 秒。

从我在 flask-sqlalchemy 源中找到的,.paginate 是这样的:

.order_by(asc('code')).limit(per_page).offset((page - 1) * per_page)

生成的 SQL 查询如下所示:

SELECT my_table.id, my_table.code, my_table.name 
FROM my_table ORDER BY my_table.code ASC 
LIMIT 10 OFFSET 1158960

当我在服务器控制台上触发它时,我意识到问题出在 ORDER BY 子句中。不知何故,它必须首先使用 ORDER BY 对整个表进行排序,然后才使用 LIMITOFFSET。但这太慢了。

解释(分析):

"Limit  (cost=470520.26..470520.26 rows=1 width=178) (actual time=12460.060..12460.061 rows=8 loops=1)"
"  ->  Sort  (cost=467626.96..470520.26 rows=1157320 width=178) (actual time=11343.220..12424.686 rows=1158968 loops=1)"
"        Sort Key: code"
"        Sort Method: external merge  Disk: 218312kB"
"        ->  Seq Scan on my_table  (cost=0.00..42518.20 rows=1157320 width=178) (actual time=0.026..378.637 rows=1158968 loops=1)"
"Total runtime: 12475.160 ms"

如果您只是从该 SQL 请求中删除 ORDER BY,它会在 270 毫秒内完成!

"Limit  (cost=42518.20..42518.20 rows=1 width=178) (actual time=269.940..269.942 rows=8 loops=1)"
"  ->  Seq Scan on my_table  (cost=0.00..42518.20 rows=1157320 width=178) (actual time=0.030..246.200 rows=1158968 loops=1)"
"Total runtime: 269.992 ms"

我能做些什么吗?

【问题讨论】:

  • 你有哪些指标?您要应用哪些过滤器?
  • Seq Scan on my_table 可能意味着索引ix_my_table_code 未用于ORDER BY 操作。我会测试reindex index ix_my_table_code 看看Index Scan using ix_my_table_code on my_table 是否出现在explain analyze ...
  • @univerio 关于索引,它们是由 flask-sqlalchemy 创建的默认索引: CREATE UNIQUE INDEX ix_my_table_code ON my_table USING btree (code COLLATE pg_catalog."default");
  • @J.J.Hakala 我按照您的建议进行了重新索引,但没有任何改变。我也不太明白 - 如果为具有默认 ASC 顺序的字段构建索引 - 为什么该字段的 ORDER_BY ... ASC 会强制执行我们在 EXPLAIN 中看到的排序操作?记录不是已经在该索引中排序了吗?
  • @Tosh 我用 postgresql 9.4 进行了测试,我不知道在这种情况下它的行为是否与 9.3 版本不同。

标签: postgresql flask pagination sqlalchemy sql-order-by


【解决方案1】:

好的,我找到了解决这个问题的方法。

当我执行完全相同的查询但使用 SET enable_seqscan=off; 时,它会强制 PostgreSQL 使用 Index Scan 而不是 Sequence Scan,它会得到 很多更快

SET enable_seqscan=off;
SELECT my_table.id, my_table.code, my_table.name 
FROM my_table ORDER BY my_table.code ASC 
LIMIT 10 OFFSET 1158960

**EXPLAIN (ANALYZE):**

"Limit  (cost=1814764.86..1814777.39 rows=8 width=131) (actual time=616.543..616.545 rows=8 loops=1)"
"  ->  Index Scan using ix_my_table_code on my_table  (cost=0.43..1814777.39 rows=1158968 width=131) (actual time=0.065..590.898 rows=1158968 loops=1)"
"Total runtime: 616.568 ms"

所以现在的重点是 - 我如何设置 PostgreSQL 配置以使其使用索引扫描而不强制它?我猜答案是 - “Planner Cost Constants”。对他们有什么建议吗?

2016 年 4 月 13 日更新:

我终于弄清楚了情况并找到了解决方案。就我而言,一切都可以通过在 postgresql.conf 中像这样设置 Planner Cost Constants 来解决:

seq_page_cost = 1.0
random_page_cost = 1.0
cpu_tuple_cost = 0.01
cpu_index_tuple_cost = 0.0001
cpu_operator_cost = 0.0025
effective_cache_size = 1024MB

此外,还有很多建议将有效缓存大小设置为服务器整个 RAM 的 3/4。无论如何,有了这些设置,Planner 总是在巨大的表上使用索引扫描。所以现在的时间是 200-300ms。

问题已解决。

【讨论】:

  • 之前没注意到OFFSET。这是一个荒谬的抵消。我想这可能就是原因。尝试删除偏移量?如果是这个原因,我的建议是使用WHERE code > [some code] ORDER BY code 而不是OFFSET [n] LIMIT [m] 进行寻呼。
  • @univerio 但我需要在前端从一个页面移动到另一个页面,知道每页的元素数量。该代码不是增量列,所以如果我想向用户显示第 12000 页,知道 per_page,你究竟如何建议获取该页面之前的最后一个元素代码,所以我可以在 WHERE 子句?而且 - 使用索引扫描它工作得很好,所以现在的重点是 - 如何从 SQLAlchemy ORM 请求中设置 enable_seqscan=off,或者更改 postgres 计划器设置以使其工作?
  • 当你有一张巨大的桌子时,你不能这样翻页。如果用户想要页面 100,000,000(偏移量 10 亿)怎么办?你会遇到完全相同的问题。解决这个问题的唯一方法是只让用户向前和向后翻页n页(最简单的情况是n = 1)并使用WHERE而不是OFFSET
  • @univerio 同意。这正是我现在正在做的事情。用户可以移动到下一页或上一页(如果存在)。但用户也应该有按钮来移动到第一页或最后一页。它们包含的记录很大程度上取决于 ORDER BY,它由用户选择的排序字段和 ASC/DESC 定义。无论如何,我深入研究了 Planner Cost Constants 文档,并对其进行了调整以使索引扫描工作而不强制 SET enable_seqscan=off;
  • @Tosh 你能告诉我如何在 MySQL 中解决这个问题吗?
猜你喜欢
  • 1970-01-01
  • 2015-12-03
  • 2018-12-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-09-02
  • 2017-06-08
  • 2017-01-07
相关资源
最近更新 更多