看似快速的过滤器字段查找速度很慢

Posted

技术标签:

【中文标题】看似快速的过滤器字段查找速度很慢【英文标题】:Seemingly quick filter field-lookup is slow 【发布时间】:2016-10-03 23:11:06 【问题描述】:

我的粗略模型:

class m_Interaction(models.Model):
    fk_ip = models.ForeignKey('m_IP', on_delete=models.SET_NULL, null=True, related_name="interactions")
    fk_query = models.ForeignKey('m_Query', on_delete=models.SET_NULL, null=True, related_name="interactions")

使用的数据库:SQLite


如果我执行这个查询集

m_Interaction.objects.filter(fk_query=None).filter(fk_ip__in=user.ips.all()).select_related('fk_query')

需要 5 秒。

如果我删除filter(fk_query=None) 语句,剩下的查询集

m_Interaction.objects.filter(fk_ip__in=user.ips.all()).select_related('fk_query')

只需 100 毫秒即可执行。

filter(fk_ip__in=user.ips.all()) 不应该贵很多吗?或者至少为什么filter(fk_query=None) 语句这么慢?它应该是一个简单的“与 Null 比较”的查找。


filter(fk_query=None) 的 SQL 查询:

SELECT "data_manager_m_interaction"."id", 
       "data_manager_m_interaction"."fk_ip_id", 
       "data_manager_m_interaction"."fk_query_id",
       "data_manager_m_query"."id", 
       "data_manager_m_query"."fk_ip_id" 
FROM "data_manager_m_interaction" 
LEFT OUTER JOIN "data_manager_m_query" 
ON ("data_manager_m_interaction"."fk_query_id" = "data_manager_m_query"."id") 
WHERE ("data_manager_m_interaction"."fk_ip_id" IN (SELECT U0."id" FROM "data_manager_m_ip" U0 WHERE U0."fk_user_id" = 1339) 
  AND "data_manager_m_interaction"."fk_query_id" IS NULL) 
ORDER BY "data_manager_m_interaction"."timestamp" ASC 
LIMIT 1

没有filter(fk_query=None)的SQL查询:

SELECT "data_manager_m_interaction"."id", 
       "data_manager_m_interaction"."fk_ip_id", 
       "data_manager_m_interaction"."fk_query_id", 
       "data_manager_m_query"."id", 
       "data_manager_m_query"."fk_ip_id" 
FROM "data_manager_m_interaction" 
LEFT OUTER JOIN "data_manager_m_query" 
ON ("data_manager_m_interaction"."fk_query_id" = "data_manager_m_query"."id") 
WHERE "data_manager_m_interaction"."fk_ip_id" IN (SELECT U0."id" FROM "data_manager_m_ip" U0 WHERE U0."fk_user_id" = 1339) 
ORDER BY "data_manager_m_interaction"."timestamp" ASC 
LIMIT 1

解释查询计划(带过滤器):

[(0, 0, 0, 'SEARCH TABLE data_manager_m_interaction USING INDEX data_manager_m_interaction_c50f4040 (fk_query_id=?)'), 
(0, 0, 0, 'EXECUTE LIST SUBQUERY 1'), 
(1, 0, 0, 'SEARCH TABLE data_manager_m_ip AS U0 USING COVERING INDEX data_manager_m_ip_f569ccde (fk_user_id=?)'), 
(0, 1, 1, 'SEARCH TABLE data_manager_m_query USING INTEGER PRIMARY KEY (rowid=?)'), 
(0, 0, 0, 'USE TEMP B-TREE FOR ORDER BY')]

解释查询计划(不带过滤器)

[(0, 0, 0, 'SEARCH TABLE data_manager_m_interaction USING INDEX data_manager_m_interaction_c669518a (fk_ip_id=?)'), 
(0, 0, 0, 'EXECUTE LIST SUBQUERY 1'), 
(1, 0, 0, 'SEARCH TABLE data_manager_m_ip AS U0 USING COVERING INDEX data_manager_m_ip_f569ccde (fk_user_id=?)'), 
(0, 1, 1, 'SEARCH TABLE data_manager_m_query USING INTEGER PRIMARY KEY (rowid=?)'), 
(0, 0, 0, 'USE TEMP B-TREE FOR ORDER BY')]

【问题讨论】:

首先,您可以尝试filter(fk_query__isnull=True) 而不是fk_query=None 看看是否有任何改进,但我会使用print your-query.query 来查看原始sql 语句并查看差异。 我同意这似乎违反直觉。如果您更改过滤器的顺序或将它们组合成一个过滤器,会有什么不同吗? filter(fk_query=None, fk_ip__in=user.ips.all()) 你在用mysql吗?我怀疑你是。如果是这样,这个结果实际上并不令人惊讶。 感谢您的快速回复!我更新了我的问题。 @Shang Wang filter(fk_query__isnull=True) 没有区别。上面列出了原始查询。 @Håken Lid 顺序无关紧要 @e4c5 我正在使用 SQLite,甚至更糟:) 但在这种情况下这不应该是问题 【参考方案1】:

sqlite 和 mysql 的问题在于它们只能使用每个表的一个索引,如 https://www.sqlite.org/optoverview.html 所述

查询的 FROM 子句中的每个表最多可以使用一个索引 (除非 OR 子句优化发挥作用)和 SQLite 力求在每张表上至少使用一个索引

更糟糕的是,因为 sqlite 查询解析器将 ON 条件转换为 WHERE 子句。即使没有IS NULL,您的WHERE 子句也相当繁重。更糟糕的是,因为你有一个订单。

SQLite 尝试使用索引来满足 尽可能查询。当面临使用索引的选择时 满足 WHERE 子句约束或满足 ORDER BY 子句, SQLite 执行与上述相同的成本分析,并选择 它认为会产生最快答案的索引。

在许多情况下,mysql 可以使用另一个索引作为 order by,但 sqlite 不能。 Postgresql,可以说是最好的开源RDBMS,可以在每个表上使用多个索引。

因此,简而言之,sqlite 无法使用索引进行IS NULL 比较。在查询中使用EXPLAIN 会显示可用索引在fk_ip_id 上使用

编辑: 我不像在 postgresql 或 mysql 上那样精通 sqlite 解释输出,但据我了解,每个表都使用一个索引,如上所述。 data_manager_m_ip 表是最能利用索引的表。那里甚至没有查看表本身,所有数据都是从索引本身检索的。

解释也确实表明使用了 fk_query_id 上的索引。但是我的理解是这用于连接。解释还表明,没有使用任何索引进行排序。您能否也发布其他查询的说明。

编辑 2: 好了,不看EXPLAIN 进行优化是很危险的。我们猜测是 null 比较很慢。但它不是!当您进行IS NULL 比较时,sqlite 会为此使用索引,但 IN 子句现在没有索引,这使得它非常慢!!

解决方案:你需要一个fk_query_id, fk_ip_id的复合索引,你可以使用django index_together来做一个。

【讨论】:

感谢您提供如此详细的分析,但这是否解释了filter(fk_query=None) 的糟糕表现?如果我只留下它,并删除 filter(fk_ip__in=user.ips.all())-part 它也表现不佳。但这可能是因为我没有为 fk_query-field 建立索引吗? 我不确定如何解释这一点,但在我看来,SQLite 似乎正在使用索引搜索由 query=null 过滤的交互。然后它通过 ips 过滤结果,也使用索引等。所以对我来说一切都很好 我必须重新索引所有内容吗?我在模型中添加了index_together = ["fk_query", "fk_ip"],并做了makemigrationsmigrate。迁移大约需要 5 分钟,所以我认为它创建了索引,但查询本身仍然很慢。 explain-query 也没有变化,应该有变化吗? 因为一张表只能使用一个索引。如果一个用于 IS NULL,则另一个测试将没有索引。 我确实尝试过修复它,但它仍然无法正常工作。我什至创建了两个复合索引,仅用于测试:index_together = [["fk_ip", "fk_query"],["fk_query", "fk_ip"]] 但它都没有使用它们。它仍然与问题中所写的explain query 相同。对我来说,它似乎每次都使用索引。 is null-lookup 的正常一个,in-lookup 的这个“覆盖”一个

以上是关于看似快速的过滤器字段查找速度很慢的主要内容,如果未能解决你的问题,请参考以下文章

Django Json 字段过滤器抛出查找错误

布隆过滤器(Bloom Filter)

布隆过滤器(Bloom Filter)

ElasticSearch结构化搜索和全文搜索

在 Django 中,如何使用动态字段查找过滤 QuerySet?

使用跨越关系的字段查找在 django 模型上进行链式过滤和排除