Столкнулись с значительные потери производительности используя django orm, я начал искать выход из ситуации, рассматривая разные способы использования orm. Что я сделал - посмотрел на подкат. Как написать обычный фрагмент кода с помощью django orm? Как правило, эта штука включается в некую функцию, например view, получает параметры и на основе этих параметров генерирует результат. В качестве примера рассмотрим следующую элементарную ситуацию: мы хотим получить список названий групп, членом которых является текущий пользователь.
Самый простой и очевидный способ сделать это, тот, который приходит в голову первым, — получить список групп через отношение и узнать их имена:
Давайте проверим, какова будет производительность этого куска, учитывая, что пользовательский объект request.user уже получен на этапе предварительной обработки запроса.def myview(request): u = request.user a = [g.name for g in u.groups.all()] .
Создадим группу thetest и добавим в нее самого первого пользователя: >>> u = User.objects.all()[0]
>>> g = Group(name='thetest')
>>> g.save()
>>> u.groups.add(g)
>>> u.groups.all()
[<Group: thetest>]
Я буду использовать этот случай во всех будущих тестах.
Поскольку все делается через оболочку, я в них также использую полученную на этом этапе переменную u. Итак, тест номер 1, выполняем намеченный фрагмент кода.
Давайте проверим, действительно ли он возвращает список, который мы ищем: >>> a = [g.name for g in u.groups.all()]
>>> a
[u'thetest']
Чтобы измерить производительность, давайте запустим его 1000 раз.
>>> def test1():
.
import datetime .
t1 = datetime.datetime.now() .
for i in xrange(1000): .
a = [g.name for g in u.groups.all()] .
t2 = datetime.datetime.now() .
print "%s" % (t2 - t1) .
>>> test1()
0:00:01.437324
Тысяча оборотов нашего цикла заняла примерно полторы секунды, что дает 1,5 миллисекунды на запрос.
Опытные писатели Django, наверное, уже ткнут мне носом, что эта штука далека от оптимальной.
Действительно, мы можем написать, казалось бы, более оптимальный кусок кода, который будет выполнять те же действия, не создавая групповой объект и получая из базы только те данные, которые нам действительно нужны: >>> a = [g['name'] for g in u.groups.values('name')]
>>> a
[u'thetest']
Что ж, давайте измерим и этот кусок.
>>> def test2():
.
import datetime .
t1 = datetime.datetime.now() .
for i in xrange(1000): .
a = [g['name'] for g in u.groups.values('name')] .
t2 = datetime.datetime.now() .
print "%s" % (t2 - t1) .
>>> test2()
0:00:01.752529
Это кажется нелогичным, но является ли вторая версия нашего кода менее оптимальной, чем первая?
На самом деле, это так.
Потери от вызова Values() и дополнительного анализа запроса оказались выше потенциальной экономии на построении объекта Group и получении значений всех его полей.
Но простите? Почему именно каждый раз перепроектировать и проанализировать запросить, всегда ли мы выполняем по нашему мнению тот же запрос , а отличаться будет только пользовательский объект, на котором выполняется этот запрос? К сожалению, Джанго является родным не позволяет подготовить запрос заранее, обращаясь к подготовленному запросу по мере необходимости.
Соответствующих вызовов нет, а синтаксис формирования запроса предполагает использование в качестве параметров запроса только определенных значений.
Вам придется немного покопаться в источниках.
Пользуясь случаем, хочу выразить благодарность разработчикам django_extensions и их замечательной командеshell_plus, которая значительно облегчает самоанализ объектов.
Оказывается, объект QuerySet (это тот самый, который получается, например, при вызове Objects.all()) имеет свойство запроса, объект класса django.db.models.sql.query.Query. Который, в свою очередь, имеет метод sql_with_params().
Этот метод возвращает набор параметров, полностью готовых для передачи в курсор.
execute(), то есть строку выражения SQL и дополнительные параметры.
Самое замечательное, что эти дополнительные параметры — это параметры, которые передаются объекту QuerySet при его формировании: >>> u.groups.all().
values('name').
query.sql_with_params() ('SELECT `auth_group`.
`name` FROM `auth_group` INNER JOIN `auth_user_groups` ON (`auth_group`.
`id` = `auth_user_groups`.
`group_id`) WHERE `auth_user_groups`.
`user_id` = %s ', (1,))
Теперь, если мы получим подготовленный SQL-запрос и подставим в него разные значения параметров, мы сможем выполнить запрос, не тратя ресурсы на подготовку запроса.
Для этого мы создадим специальный класс, который спрячет внутри все детали взлома, который мы собираемся выполнить.
from django.db import connection
from django.db.models.query import QuerySet,ValuesQuerySet
import django
from threading import local
class PQuery(local):
def __init__(self,query,connection=connection,**placeholders):
self.query = query
self.connection = connection
self.placeholders = placeholders
self.replaces = {}
sql = None
try:
sql = self.query.query.sql_with_params() # 1.4
except AttributeError:
sql = self.query.query.get_compiler(connection=self.connection).
as_sql() # 1.3, lower? self.places = list(sql[1]) self.sql = sql[0] self.is_values = isinstance(query,ValuesQuerySet) self.cursor = None for i in xrange(len(self.places)): x = self.places[i] found = False for p in self.placeholders: v = self.placeholders[p] if x == v: found = True if not p in self.replaces: self.replaces[p] = [] self.replaces[p].
append(i)
if not found:
raise AttributeError("The placeholder %(ph)s not found, please add some_name=%(ph)s to the list of constructor parameters" % {
'ph':repr(x)
})
def execute(self,**kw):
try:
for k in kw:
for i in self.replaces[k]:
self.places[i] = kw[k]
except KeyError,ex:
raise TypeError("No such placeholder: %s" % k)
if not self.cursor:
self.cursor = self.connection.cursor()
self.cursor.execute(self.sql,self.places)
if not hasattr(self,'fldnms'):
self.fldnms = [col[0] for col in self.cursor.description]
if self.is_values:
return [dict(zip(self.fldnms,row)) for row in self.cursor.fetchall()]
return [self.query.model(**dict(zip(self.fldnms,row))) for row in self.cursor.fetchall()]
def __call__(self,**kw):
return self.execute(**kw)
ParametrizedQuery = PQuery # compatibility issue
UPD: 06-08-2012, 19:20 MSK - внесены изменения в код относительно совместимости с многопоточностью, исправление мелких ошибок при выполнении сложных запросов и повышение удобства использования.
Предыдущая версия кода from django.db import connection
from django.db.models.query import QuerySet,ValuesQuerySet
class ParametrizedQuery:
def __init__(self,query,connection=connection,**placeholders):
self.query = query
self.connection = connection
self.placeholders = placeholders
self.replaces = {}
sql = self.query.query.sql_with_params()
self.places = list(sql[1])
self.sql = sql[0]
self.is_values = isinstance(query,ValuesQuerySet)
self.cursor = None
for p in self.placeholders:
v = self.placeholders[p]
self.replaces[p] = self.places.index(v)
def execute(self,**kw):
for k in kw:
self.places[self.replaces[k]] = kw[k]
if not self.cursor:
self.cursor = self.connection.cursor()
self.cursor.execute(self.sql,self.places)
if not hasattr(self,'fldnms'):
self.fldnms = [col[0] for col in self.cursor.description]
if self.is_values:
return [dict(zip(self.fldnms,row)) for row in self.cursor.fetchall()]
return [self.query.model(**dict(zip(self.fldnms,row))) for row in self.cursor.fetchall()]
Что делает этот класс? Он получает запрос и извлекает из него подготовленный SQL и параметры.
Мы можем создать запрос, в котором каждый из параметров, которые мы собираемся подставлять, имеет особое значение, известное нам заранее.
Мы будем использовать эти значения, чтобы найти место, куда мы хотим подставить значения, переданные во время выполнения.
Несколько дополнительных деталей реализации также помогут нам сэкономить ресурсы.
- Свойство fldnms содержит массив имен полей, полученных при первом выполнении запроса.
Последующие вызовы будут использовать подготовленный массив.
- Свойство replaces содержит сопоставление имен подстановок с номерами параметров.
- Каждый объект нашего класса будет содержать собственный курсор.
Потенциальное ускорение от этого шага является следствием, во-первых, того, что создание курсора — достаточно затратная операция, во-вторых, следующей фразы из описания pyodbc , который можно использовать в качестве базы данных: «Также будет более эффективно, если вы будете многократно выполнять один и тот же SQL с разными параметрами.
SQL будет подготовлен только один раз.
(pyodbc сохраняет подготовленным только последний оператор, поэтому, если вы переключаетесь между операторами, каждый из них будет подготовлен несколько раз.
)"
- Свойство is_values поможет нам определить, что запрос не должен возвращать объект модели, что позволит сэкономить на создании такого объекта при возврате результатов.
>>> q = Group.objects.filter(user__id=12345).
values('name')
>>> q.query.sql_with_params()
('SELECT `auth_group`.
`name` FROM `auth_group` INNER JOIN `auth_user_groups` ON (`auth_group`.
`id` = `auth_user_groups`.
`group_id`) WHERE `auth_user_groups`.
`user_id` = %s ', (12345,))
В качестве замены мы используем значение 12345:
>>> p = ParametrizedQuery(q,user_id=12345)
>>> [g['name'] for g in p.execute(user_id=u.id)]
[u'thetest']
При выполнении запроса p.execute() место подстановки 12345 было заменено реальным значением идентификатора пользователя.
Давайте теперь попробуем посмотреть, как изменится производительность кода: >>> def test3():
.
import datetime .
t1 = datetime.datetime.now() .
for i in xrange(1000): .
a = [g['name'] for g in p.execute(user_id=u.id)] .
t2 = datetime.datetime.now() .
print "%s" % (t2 - t1) .
>>> test3()
0:00:00.217270
Вот результат! Время выполнения запроса сокращено в 7 раз .
Как использовать это в реальном коде? Во-первых, вам нужно место, где можно было бы хранить подготовленный запрос.
Во-вторых, в какой-то момент эту переменную необходимо заполнить.
Например, в момент первого выполнения кода функции.
И в-третьих, конечно, мы используем доступ к параметризованному запросу вместо непосредственного выполнения запроса.
def myview(request):
if not hasattr(myview,'query'):
myview.query = ParametrizedQuery(Group.objects.filter(user__id=12345).
values('name'),user_id=12345) a = [g['name'] for g in myview.query.execute(user_id=request.user.id)] .
Весь код был выполнен:
— django.VERSION=(1, 4, 0, 'окончательный', 0)
— СУБД mysql (django.db.backends.mysql)
- механизм таблицы=MYISAM
- подключение через локалхост
- Python 2.7.2+ (по умолчанию, 4 октября 2011 г.
, 20:03:08) [GCC 4.6.1] на linux2 - хост Linux seva 3.0.0-22-generic #36-Ubuntu SMP Вт, 12 июня 17:13:04 UTC 2012 i686 athlon i386 GNU/Linux Комментарии экспертов приветствуются.
Теги: #разработка для #производительности #orm #django #django orm #параметризация запросов #Высокая производительность #python #django
-
Как Открыть Интернет-Магазин Бесплатно
19 Oct, 24 -
Монолитные Системы – Наследие
19 Oct, 24 -
Бфм.ру
19 Oct, 24 -
Клуб Инноваторов Теперь В Нижнем Новгороде
19 Oct, 24 -
«Эксперт» Выпустил «Эксперт Онлайн 2.0»
19 Oct, 24