看似快速的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 来制作一个。
我的粗略模型:
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 来制作一个。