Pull to refresh

Comments 19

По мне так, код не особо «красивым» получается. Мне больше нравится такая система:
q = query('table', 'alias').select('a', 'b', 'c').where(a = 'b', b = 2).orderBy('a', true);
Реализованы классы у меня под python и php для mysql и postgresql. В скором времени, наверное, выложу на гитхаб и накатаю статейку.
вы просто создали еще один способ записать неудобно sql.
1.5 мс на выполнение запроса к БД — это не много. Не самое критичное место в производительности веб приложений. Тот же рендеринг шаблона у вас займет явно больше.
^ this. Особенно, если шаблоны рендерятся обычными джанговыми темплейтами.
Сильно подозреваю (но еще не проверял), что одним из основных мест торможения при рендеринге шаблонов может стать как раз доступ к объектам через ORM. Если вы используете в шаблоне например тот же пресловутый список групп, получая его допустим через выражение наподобие
<ul>
{% for u in user.groups.all %}
  <li>{{ u.name }}</li>
{% endfor %}
</ul>

то при рендеринге такого шаблона как раз и случится описанный в топике тормоз.
А вот так не пробовали?
q = Group.objects.values('name')
..
a = q.filter(user=u)

Без всяких хаков дало прирост в 10 раз.
Бало как и у вас 1.6 сек, стало 0.15 сек.
a = q.filter(user=u) возвращает подготовленный QuerySet, а вовсе не список значений. Попробуйте a=list(q.filter(user=u)) и вы увидите разницу
Вы правы, об этом я не подумал.
Мне кажется, подход правильный, понравилось.

1) Вроде лучше было в бенчмарках использовать

.values_list('name', flat=True)

вместо

[g['name'] for g in u.groups.values('name')].

2) Не очень приятная штука — в Group.objects.filter(user__id=12345).values('name') используется user__id, а в ParametrizedQuery — user_id. Было б удобно, если бы все было одинаково, или хотя бы было очевидно, как имена получать. Ведь если бы запрос был с user__username, нужно было бы писать не user_username, а просто username, так? Или еще что-то другое? Это все неочевидно довольно.

3) Пример из реального кода не понял — зачем там 12345. Ну и код с багами на потокобезопасность — у вас общий query на все потоки (т.к. он атрибут myview), execute меняет некоторые атрибуты query (self.places, например) — в момент вызова self.cursor.execute(self.sql,self.places) уже нельзя гарантировать, что в self.places были на основе **kw из этого потока построены.

Тут, мне кажется, 2 вещи: (а) .execute не должен менять состояние и (б) подготовленный запрос создать бы вне вьюхи (лучше всего — в models.py или managers.py; самое правильное — вообще в менеджере или в кастомном QuerySet для модели) — там же ничего зависящего от request нет, и несколько экземпляров не нужны, если execute состояние менять не будет.
А по реализации — через RawQuerySet этот хак не получается сделать? Туда и query можно передать готовый (не raw_query), и params + логика по построению моделей более хитрая уже реализована — например, части ответа могут быть доступны как атрибуты модели, но не будут передаваться в конструктор (какое-нибудь вычисляемое в sql-запросе поле, например). Сам не пробовал, но код похоже выглядит.
1) values_list погоды не делает, тормозит практически так же
2,3) user__id — это обращение к полю id объекта user, в то время как user_id — это вообще говоря произвольное имя параметра параметризованного запроса, там мог бы стоять например vasq_pupkin:

>>> p = ParametrizedQuery(q,vasq_pupkin=12345)
>>> [g['name'] for g in p.execute(vasq_pupkin=u.id)]


Идея заключается в том, что значение 12345 в исходном запросе QuerySet является отметкой того места, куда должно попасть значение соответствующего параметра ParametrizedQuery при его фактическом использовании. То есть последовательность следующая:
  • В исходном запросе мы отмечаем фейковыми значениями (12345, «QQ», datetime.datetime(2345,11,11) и т.д.) те места, которые будут заполняться фактическими значениями параметров при выполнении запроса.
  • Формируя параметризованный запрос, мы даем имена параметрам запроса и связываем эти имена с теми местами, в которые должны попасть фактически переданные значения параметров. Связывание происходит через те фейковые значения, которые переданы в исходный запрос.
  • При использовании параметризованного запроса, мы передаем фактические значения через те параметры, которые были поименованы при формировании параметризованного запроса. Эти значения заменяют собой фейковые значения, переданные при формировании исходного запроса

Это в некотором смысле извращение, которое продиктовано тем, что django не имеет синтаксиса, подходящего для передачи в запрос формальных параметров.

3) Про многопоточность — да, я заметил эту проблему и собираюсь как раз сейчас сделать апдейт поста с поправками по поводу многопоточности и еще несколько мелких недочетов.
In [10]: %time t=[u.groups.all() for i in range(1000)]
CPU times: user 0.63 s, sys: 0.01 s, total: 0.64 s
Wall time: 0.64 s

In [11]: %time t=[u.groups.values('name') for i in range(1000)]
CPU times: user 1.03 s, sys: 0.02 s, total: 1.06 s
Wall time: 1.04 s

На самом деле, если посмотреть в connection.queries из django.db.models, то первый и второй запрос к базе практически не отличаются. Хотя потеря почти в ~0.40с.

Действительно дело состоит в скорости работы самого django orm. Если честно то ситуация с таким подходом «разгона» orm как минимум занятная. Так как от мы теряем по сути большую часть общего функционала, но получаем быстрый кусок точных данных. Если честно, то мне скорее непонятно где конкретно это может пригодится.
u.groups.all() не возвращает данные. Он возвращает QuerySet. Правильно мерять надо было так:

%time t=[list(u.groups.all()) for i in range(1000)]
%time t=[list(u.groups.values('name')) for i in range(1000)]

Кстати, это же ipython? Тем через %timeit можно замеры делать — он и количество итераций сам выберет, и несколько раз бенчмарк прогонит, и сборщик мусора отключит, как-то так:

%timeit list(u.groups.all())
Данные в данном ключе не принципиальны, так как вопрос стоит в самой работе orm, а не оптимизации запроса к базе. А вот расхождение между .values('names') и all() действительно значительны, при учете того, что работает только ORM:

Без получения данных:
In [14]: %time t=[u.groups.all() for i in range(1000)]
CPU times: user 0.69 s, sys: 0.01 s, total: 0.70 s
Wall time: 0.69 s

In [15]: %time t=[u.groups.values('name') for i in range(1000)]
CPU times: user 1.04 s, sys: 0.01 s, total: 1.05 s
Wall time: 1.04 s

С получением:
In [17]: %time t=[list(u.groups.all()) for i in range(1000)]
CPU times: user 1.03 s, sys: 0.02 s, total: 1.05 s
Wall time: 1.05 s

In [18]: %time t=[list(u.groups.values('name')) for i in range(1000)]
CPU times: user 1.36 s, sys: 0.02 s, total: 1.38 s
Wall time: 1.39 s

Это примерно показывает, то что и должно: запросы и в первом случае и во втором практически одинаковы, но тем не менее .values('names') работает значительно медленнее.

Спасибо кстати за наводку, если бы не прочитал статью не думал бы, что values медленнее настолько :)
Потери на 50% на values это плохо конечно. Но меня лично прикалывает объем потерь именно на коде, который мог бы быть вынесен за пределы рабочего цикла вообще — компиляция запроса sql.
It is also be more efficient if you execute the same SQL repeatedly with different parameters. The SQL will be prepared only once.

хочется отметить, что настоящие prepared statements связаны с выделением долгоживущих ресурсов. т.е. одно дело служебные скрипты, но если клиентов много (как в web), такую схему нужно обязательно тестировать на нагрузки. к тому же они поддерживаются далеко не всеми питонячими драйверами и имеют характерные ограничения.
В классической конфигурации django-wsgi — клиентов (БД) будет ровно столько, сколько WSGI исполнителей. Количество препарированных выражений будет пропорционально количеству строк кода, таким образом нагрузка легко измерима и контролируема. Впрочем конечно же, тестировать надо всегда.

Из питонячьих дриверов упоминание о prepared statements мне удалось найти только в pyodbc. Структура интерфейса «стандартного клиента СУБД» к сожалению, провоцирует разработчиков игнорировать prepared statements — поскольку не поддерживает явного обращения к ним.
для mysql prepared statements поддерживаются драйвером oursql.
Sign up to leave a comment.

Articles