在 Postgres DB 中使用大量 id 的 Django 查询过滤器

Posted

技术标签:

【中文标题】在 Postgres DB 中使用大量 id 的 Django 查询过滤器【英文标题】:Django query filter using large array of ids in Postgres DB 【发布时间】:2018-03-25 20:46:42 【问题描述】:

我想将 Django 中的查询传递给我的 PostgreSQL 数据库。当我使用大量 id 过滤查询时,查询速度非常慢,最多 70 秒。

在寻找答案后,我看到 this post 可以解决我的问题,只需将 IN 语句中的 ARRAY [ids] 更改为 VALUES (id1), (id2), ...

我在 pgadmin 中使用原始查询测试了该解决方案,查询从 70 秒到 300 毫秒...

如何在 Django 中执行相同的命令(即不使用 id 数组,而是使用 VALUES 进行查询)?

【问题讨论】:

您能发布您实际的 django 过滤器查询吗? 【参考方案1】:

我使用custom lookup 找到了基于@erwin-brandstetter 答案的解决方案

from django.db.models import Lookup
from django.db.models.fields import Field

@Field.register_lookup
class EfficientInLookup(Lookup):

    lookup_name = "ineff"

    def as_sql(self, compiler, connection):
        lhs, lhs_params = self.process_lhs(compiler, connection)
        rhs, rhs_params = self.process_rhs(compiler, connection)
        params = lhs_params + rhs_params
        return "%s IN (SELECT unnest(%s))" % (lhs, rhs), params

这允许像这样过滤:

MyModel.objects.filter(id__ineff=<list-of-values>)

【讨论】:

将列表传递给标准的__in 查找时,Django 2.1 将其转换为IN (x, y, z)。此外,问题中引用的帖子已更新为“只需要在 9.0 上应用。从 Postgres 9.3 开始,ANY(ARRAY[...]) 再次正常执行。”所以这可能只与旧版本的 Django Postgres有关。 但是很高兴看到可以用作其他工作模板的自定义查找工作! 好吧,我认为这对于大型列表来说仍然是一个问题。对于包含 2k 个条目的列表,我可以通过上面的查找将查询时间从 2 秒减少到 0.05 秒!我正在使用 postgres 10 和 django 2.1 这绝对是重要的。导致使用 ARRAY 进行 SQL 查询的 ORM 调用是什么? 我似乎无法复制这一点。 2k 列表需要 15 毫秒,50k (!) 列表需要 250 毫秒。【参考方案2】:

诀窍是将 数组 转换为 set 某种方式

而不是(这种形式只适用于短数组):

SELECT *
FROM   tbl t
WHERE  t.tbl_id = ANY($1);
-- WHERE  t.tbl_id IN($1);  -- equivalent

$1为数组参数。

您仍然可以像以前一样传递 数组,但要取消嵌套并加入。喜欢:

SELECT *
FROM   tbl t
JOIN   unnest($1) arr(id) ON arr.id = t.tbl_id;

或者你也可以保留你的查询,但是用一个取消嵌套的子查询替换数组:

SELECT * FROM tbl t
WHERE  t.tbl_id = ANY (SELECT unnest($1));

或者:

SELECT * FROM tbl t
WHERE  t.tbl_id IN    (SELECT unnest($1));

与通过 VALUES 表达式传递 set 的性能效果相同。但是传递数组通常要简单得多。

详细解释:

IN vs ANY operator in PostgreSQL How to use ANY instead of IN in a WHERE clause with Rails? Optimizing a Postgres query with a large IN

【讨论】:

【参考方案3】:

这是你问的第一件事的一个例子吗?

relation_list = list(ModelA.objects.filter(id__gt=100))
obj_query = ModelB.objects.filter(a_relation__in=relation_list)

这将是一个“IN”命令,因为您首先通过将 relation_list 转换为 list 来评估它,然后在您的第二个查询中使用它。

如果你做同样的事情,Django 只会做一个查询,并为你做 SQL 优化。所以这样应该会更有效率。

如果您对幕后发生的事情感到好奇,您总是可以看到您将使用 obj_query.query 执行的 SQL 命令。

希望能回答这个问题,如果没有,抱歉。

【讨论】:

【参考方案4】:

让自定义查找“无效”工作时遇到了很多麻烦。 我可能已经解决了它,但我希望得到 Django 和 Postgres 专家的一些验证。

1) 在 ForeignKey 字段 (ModelB) 上“直接”使用它

ModelA.objects.filter(ModelB__ineff=queryset_ModelB)

抛出以下异常: “相关字段查找无效:​​ineff”

ForeignKey 字段不能用于自定义查找。

这里报告了一个类似的问题: Custom lookup is not being registered in Django

2) 在相关模型 (ModelB.id) 的 pk 字段中“间接”使用它

ModelA.objects.filter(ModelB__id__ineff=queryset_ModelB.values_list('id', flat=True))

抛出以下异常: “只能将列表(不是“元组”)连接到列表中”

查看 Django Traceback,我注意到 rhs_params 是一个元组。 然而,我们尝试在自定义查找中将其添加到 lhs_params(一个列表)。

因此我改变了:

params = lhs_params + rhs_params

进入:

params = lhs_params + list(rhs_params)

3) 然后我得到一个 Postgres 错误(至少我通过了 Django ORM) “函数unnest(uuid)不存在” “提示:没有函数与给定的名称和参数类型匹配。您可能需要添加显式类型转换。”

我显然通过更改 sql 解决了它:

来自:

return "%s IN (SELECT unnest(%s))" % (lhs, rhs), params

到:

return "%s IN (SELECT unnest(ARRAY(%s)))" % (lhs, rhs), params    

因此我的最终 as_sql 方法如下所示:

def as_sql(self, compiler, connection):
    lhs, lhs_params = self.process_lhs(compiler, connection)
    rhs, rhs_params = self.process_rhs(compiler, connection)
    params = lhs_params + list(rhs_params)
    return "%s IN (SELECT unnest(ARRAY(%s)))" % (lhs, rhs), params

它似乎有效,并且确实比 in__ 更快(在 Postgres 中使用 EXPLAIN ANALYZE 进行了测试)。 但我很想得到专家的一些验证,也许是 Erwin Brandstetter? 感谢您的意见。

【讨论】:

以上是关于在 Postgres DB 中使用大量 id 的 Django 查询过滤器的主要内容,如果未能解决你的问题,请参考以下文章

使用 SQL 列出 Postgres db 8.1 中的所有序列

Django ORM 在 Postgres DB 上留下空闲连接

Postgres 聚合 Hstore

在 postgres 查询中获取第一行

使用 Postgres 插入数据并设置外键

在带有 C 扩展名的 Postgres DB 中使用时区存储时间戳?