Django QuerySet 与原始查询性能

Posted

技术标签:

【中文标题】Django QuerySet 与原始查询性能【英文标题】:Django QuerySet vs Raw Query performance 【发布时间】:2016-02-16 23:57:08 【问题描述】:

我注意到使用 django connection.cursor 与使用模型接口之间存在巨大的时间差异,即使是小的查询集也是如此。 我使用 values_list 使模型接口尽可能高效,因此不会构造任何对象等。下面是测试的两个函数,不要介意西班牙名字。

def t3():
    q = "select id, numerosDisponibles FROM samibackend_eventoagendado LIMIT 1000"
    with connection.cursor() as c:
        c.execute(q)
        return list(c)

def t4():
    return list(EventoAgendado.objects.all().values_list('id','numerosDisponibles')[:1000])

然后使用一个函数来计时(使用 time.clock() 自制)

r1 = timeme(t3); r2 = timeme(t4)

结果如下: t3 和 t4 为 0.00180384529631 和 0.00493390727024

只是为了确保查询是相同的并执行:

connection.queries[-2::]

产量:

[
    u'sql': u'select id, numerosDisponibles FROM samibackend_eventoagendado LIMIT 1000',  u'time': u'0.002',
    u'sql': u'SELECT `samiBackend_eventoagendado`.`id`, `samiBackend_eventoagendado`.`numerosDisponibles` FROM `samiBackend_eventoagendado` LIMIT 1000', u'time': u'0.002'
]

如您所见,两个精确查询,返回两个精确列表(执行 r1 == r2 返回 True),采用完全不同的时间(查询集越大,差异越大),我知道 python 很慢,但 django在幕后做了这么多工作以使查询变慢? 此外,为了确保,我已经尝试首先构建查询集对象(在计时器之外),但结果是相同的,所以我 100% 确定额外的时间来自获取和构建结果结构。 我也尝试在查询结束时使用 iterator() 函数,但这也无济于事。 我知道差异很小,两者的执行速度都非常快,但这是用 apache ab 进行基准测试的,当有 1k 个并发请求时,这个最小的差异让天和轻。

顺便说一句,我使用 django 1.7.10 和 mysqlclient 作为数据库连接器。

编辑:为了比较,使用 11k 结果查询集的相同测试,差异变得更大(慢 3 倍,与第一个慢约 2.6 倍相比)

r1 = timeme(t3); r2 = timeme(t4)
0.0149241530889
0.0437563529558

EDIT2:另一个有趣的测试,如果我实际上将查询集对象转换为它的实际字符串查询(使用 str(queryset.query)),并在原始查询中使用它,我将获得与原始查询相同的良好性能,通过使用 queryset.query 字符串有时会给我一个实际无效的 SQL 查询(即,如果查询集对日期值有过滤器,则日期值不会在字符串查询上用 '' 转义,给出一个 sql使用原始查询执行时出错,这是另一个谜)

-- EDIT3: 通过代码,似乎差异在于如何检索结果数据,对于原始查询集,它只是调用iter(self.cursor),我相信当使用 C 实现的连接器时,它将全部在 C 代码中运行(因为 iter 是也是一个内置的),而 ValuesListQuerySet 实际上是一个带有 yield tuple(row) 语句的 python 级别的循环,这会很慢。我想在这件事上没有什么可做的,以具有与原始查询集相同的性能:'(。 如果有人有兴趣,慢循环就是这个:

for row in self.query.get_compiler(self.db).results_iter():
    yield tuple(row)

-- 编辑 4: 我提供了一个非常 hacky 的代码,用于将值列表查询集转换为可用数据以发送到原始查询,具有与运行原始查询相同的性能,我想这很糟糕,只能与 mysql 一起使用,但是,加速非常好,同时允许我保持模型 api 过滤等。你怎么看? 这是代码。

def querysetAsRaw(qs):
    q = qs.query.get_compiler(qs.db).as_sql()
    with connection.cursor() as c:
        c.execute(q[0], q[1])
        return c

【问题讨论】:

***.com/questions/25696120/… 抱歉,在返回实际对象时会出现这种情况,但是当使用 values_list 时,它返回一个列表,它应该与使用 raw '几乎'相同 values_list 是 ORM 的一部分,它的结果类型是 ValuesListQuerySet,所以我仍然认为 django 在运行底层原始 sql 后正在处理格式。 是的,我查看了代码,它执行了大量工作,此外,正如我在编辑中添加的那样,当使用原始查询集时,它只是从连接器返回的数据中返回一个迭代器,如果使用 C 实现的连接器,它将全部在 C 中执行,而使用查询集则使用带有 yied 的 for 循环,它将在 python 中执行并且速度要慢得多。我希望有一个简单的选项可以将查询集转换为原始查询 11,000 个结果之间的 3 毫秒差异真的那么重要,以至于您会像这样破坏 ORM 吗? 虽然理论上很有趣,但您可能会限制您的部署策略并消除任何便利Django 补充道。 【参考方案1】:

答案很简单,更新到 django 1.8 或更高版本,改变了一些性能不再有这个问题的代码。

【讨论】:

以上是关于Django QuerySet 与原始查询性能的主要内容,如果未能解决你的问题,请参考以下文章

Django - 原始 SQL 查询或 Django QuerySet ORM

Django 1.9:为 QuerySet 创建复杂的自定义过滤器方法

为啥通过 django QuerySet 进行查询比在 Django 中使用游标慢得多?

Django开发博客系统(05-QuerySet的使用)

Django中的数据库查询

与原始 sql 相比,Django ORM 性能较差