【问题标题】:Performance issue with django excludedjango exclude 的性能问题
【发布时间】:2015-10-14 11:24:59
【问题描述】:

我有一个 Django 1.8 应用程序,我使用的是 MsSQL 数据库,以 pyodbc 作为数据库后端(使用“django-pyodbc-azure”模块)。

我有以下型号:

class Branch(models.Model):
    name = models.CharField(max_length=30)
    startTime = models.DateTimeField()

class Device(models.Model):
    uid = models.CharField(max_length=100, primary_key=True)
    type = models.CharField(max_length=20)
    firstSeen = models.DateTimeField()
    lastSeen = models.DateTimeField()

class Session(models.Model):
    device = models.ForeignKey(Device)
    branch = models.ForeignKey(Branch)
    start = models.DateTimeField()
    end = models.DateTimeField(null=True, blank=True)

我需要查询会话模型,我想排除一些具有特定设备值的记录。所以我发出以下查询:

sessionCount = Session.objects.filter(branch=branch)
                          .exclude(device__in=badDevices)                                             
                          .filter(end__gte=F('start')+timedelta(minutes=30)).count()

badDevices 是一个包含大约 60 项的设备 ID 的预填充列表。

badDevices = ['id-1', 'id-2', ...]

此查询大约需要 1.5 秒才能完成。如果我从查询中删除排除项,大约需要 250 毫秒。

我为这个查询集打印了生成的 sql,并在我的数据库客户端中进行了尝试。在那里,两个版本都在大约 250 毫秒内执行。

这是生成的 SQL:

SELECT [session].[id], [session].[device_id], [session].[branch_id], [session].[start], [session].[end] 
FROM [session] 
WHERE ([session].[branch_id] = my-branch-id AND 
NOT ([session].[device_id] IN ('id-1', 'id-2', 'id-3',...)) AND 
DATEPART(dw, [session].[start]) = 1 
AND [session].[end] IS NOT NULL AND 
[session].[end] >= ((DATEADD(second, 600, CAST([session].[start] AS datetime)))))

因此,在数据库级别使用排除似乎不会影响查询性能,但在 django 中,如果我添加排除部分,查询运行速度会慢 6 倍。这可能是什么原因造成的?

【问题讨论】:

  • 指出您正在使用的数据库后端可能对您有所帮助。 (即 pyodbc 实现或 Django-mssql)
  • @Ringil 感谢您的建议,用该信息更新了问题

标签: sql-server django django-pyodbc


【解决方案1】:

一般问题似乎是 django 正在做一些额外的工作来准备 exclude 子句。在这一步之后,当 SQL 生成并发送到数据库时,django 端没有发生任何可能导致如此显着延迟的有趣事件。

在您的情况下,可能导致这种情况的一件事是badDevices 的某种预处理。例如,如果badDevicesQuerySet,那么django 可能正在执行badDevices 查询只是为了准备实际查询的SQL。在device 具有非默认主键的情况下,可能会发生类似的情况。

另一件事可能会延迟 SQL 准备当然是django-pyodbc-azure。也许它在编译查询时做了一些奇怪的事情,它成为了一个瓶颈。

不过,这都是胡乱猜测,所以如果您仍然遇到此问题,请同时发布 DeviceBranch 模型、badDevices 的确切内容以及查询生成的 SQL。那么也许至少可以消除一些场景。

编辑:我认为它必须是 Device.uid 字段。可能 django 或 pyodbc 对非默认主键感到困惑,并在生成查询时获取所有设备。尝试两件事:

  • device__in 替换为device_id__indevice__pk__indevice__uid__in 并再次检查每一项。也许更明确的查询会更容易让 django 转换成 SQL。您甚至可以尝试将branch 替换为branch_id,以防万一。

  • 如果上述方法不起作用,请尝试用原始 SQL where 子句替换排除表达式:

    # add quotes (because of the hyphens) & join
    badDevicesIdString = ", ".join(["'%s'" % id for id in badDevices])
    
    # Replaces .exclude()
    ... .extra(where=['device_id NOT IN (%s)' % badDevicesIdString])
    

如果两者都不起作用,那么问题很可能出在整个查询上,而不仅仅是exclude。在这种情况下还有更多选择,但请先尝试上述方法,如有必要,我稍后会更新我的答案。

【讨论】:

  • badDevices 是一个 python 列表,而不是一个查询集,所以很可能这不会影响查询性能。设备具有非默认主键,这将如何影响查询性能?我已经编辑了问题以添加其他模型和生成的 sql。
  • 用您的建议替换 device__in 子句没有任何区别,但是您的第二个建议解决了问题,查询的性能与现在一样,谢谢。任何想法为什么这有效?
  • 目前很难不揣测,但大的延迟只能用查询编译期间到DB的往返来解释。我怀疑在编译 __in 子句时,django 看到一堆非数字 id,感到困惑,选择那些 Device 记录,然后再次提取主键(或丢弃结果)。像这样的问题并不少见,尤其是第三方后端,不幸的是,它们无法被测试套件捕获。我会尝试进一步研究它,看看我是否能找到确切的原因。
【解决方案2】:

只是想分享一下我在使用 MySQL 和排除子句时遇到的类似问题以及它是如何解决的。

运行 exclude 子句时,带有“in”查找的列表实际上是我使用 values_list 方法获得的查询集。检查 MySQL 执行的排除查询,“in”对象不是值,而是另一个查询。这种行为会影响特定大型查询的性能。

为了解决这个问题,我没有传递查询集,而是将其平铺在一个 python 值列表中。通过这样做,每个值都作为参数在 in 查找中传递,性能确实得到了提高。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-02-01
    • 2020-11-15
    • 2020-04-01
    • 1970-01-01
    • 2012-03-31
    • 1970-01-01
    • 2017-03-24
    • 1970-01-01
    相关资源
    最近更新 更多