看似快速的filter field-lookup很慢

Seemingly quick filter field-lookup is slow

我的粗略模型:

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)语句,剩下的query-set

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) 语句这么慢?它应该是一个简单的 "comparison with Null"-lookup.


SQL-用filter(fk_query=None)查询:

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

SQL-不带filter(fk_query=None)的查询:

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')]

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

所述

Each table in the FROM clause of a query can use at most one index (except when the OR-clause optimization comes into play) and SQLite strives to use at least one index on each table

而且情况变得更糟,因为 sqlite 查询解析器将 ON 条件转换为 WHERE 子句。即使没有 IS NULL 你的 WHERE 子句也相当沉重。而且情况变得更糟,因为您有订单。

SQLite attempts to use an index to satisfy the ORDER BY clause of a query when possible. When faced with the choice of using an index to satisfy WHERE clause constraints or satisfying an ORDER BY clause, SQLite does the same cost analysis described above and chooses the index that it believes will result in the fastest answer.

在许多情况下 mysql 可以使用另一个索引作为排序依据,但 sqlite 不能。 Postgresql,可以说是最好的开源 RDBMS 可以在每个 table.

上使用多个索引

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

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

解释也确实表明使用了 fk_query_id 上的索引。但是我的理解是这是用于连接的。解释还表明没有使用索引进行排序。您也可以 post 解释其他查询吗?

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

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