Пользователь
0,0
рейтинг
3 августа 2013 в 02:30

Разработка → Снова о производительности ORM, или новый перспективный проект — Pony ORM

В своей первой статье на Хабрахабре я писал об одной из основных проблем существующих ORM (Object-Relational-Mapping, объектно-реляционных отображений) — их производительности. Рассматривая и тестируя две из наиболее популярных и известных реализаций ORM на python, Django и SQLAlchemy, я пришел к выводу: Использование мощных универсальных ORM приводит к очень заметным потерям производительности. В случае использования быстрых движков СУБД, таких как MySQL — производительность доступа к данным снижается более чем в 3-5 раз.

Недавно со мной связался один из разработчиков нового движка ORM под названием pony и попросил поделиться своими соображениями по поводу этого движка. Я подумал, что эти соображения могут быть интересны и сообществу Хабрахабр.


Краткое резюме



Я вновь провел некоторые тесты производительности, сходные с описанными в предыдущей статье, и сравнил их результаты с результатами, показанными pony ORM. Для измерения производительности в условиях кешированного параметризованного запроса, мне пришлось видоизменить тест получения объекта так, чтобы каждый новый запрос получал объект с новым ключом.

Результат: pony ORM превосходит лучшие результаты django и SQLAlchemy в 1.5-3 раза, даже без кеширования объектов.

Почему pony оказался лучше



Сразу признаюсь: мне не удалось штатными средствами поставить в равные условия pony ORM с django и SQLAlchemy. Произошло это потому, что если в django можно кешировать только сконструированные конкретные запросы, а в SQLAlchemy — подготовленные параметризованные запросы (при некоторых нетривиальных усилиях), то pony ORM кеширует все, что только можно. Просмотр текста pony ORM по диагонали показал: кешируются

— готовый текст запроса SQL конкретной СУБД
— структура компилированного из текста запроса
— трансляция отношений
— соединения
— создаваемые объекты
— прочитанные и измененные объекты
— запросы для отложенного чтения
— запросы для создания, обновления и удаления объектов
— запросы для поиска объектов
— запросы блокировки
— запросы для навигации по отношениям и их модификации
— может быть еще что-то, что я пропустил

Такое кеширование позволяет выполнять код, который его использует, настолько быстро, насколько это только вообще возможно, не озабочиваясь при этом хитрыми выкрутасами по поводу повышения производительности наподобие того, который я с отчаяния придумал и описал здесь в одной из своих предыдущих статей.

Конечно, кеширование иногда приносит некоторые неудобства. Например, кеширование объектов не позволяет легко сравнить имеющееся в памяти состояние объекта с его образом в таблице — это иногда нужно для корректной обработки данных, конкурентно обрабатываемых в разных процессах. В одном из релизов pony мне лично хотелось бы увидеть параметры, позволяющие по желанию отключать отдельные виды кеширования для участка кода.

Пожелания



Чего мне пока не хватает в pony ORM, чтобы полноценно сравнить ее с другими ORM?

— миграция данных — совершенно необходимая процедура для больших проектов, использующих ORM
— адаптеры к некоторым популярным СУБД, например MS SQL
— полное абстрагирование от разновидности СУБД в коде
— доступ к полным метаданным объекта
— кастомизация типов полей
— полная документация

Чего мне не хватает в современных ORM, что можно было бы воплотить в pony ORM, пока этот проект еще не разросся до состояния стагнации?

— использование смешанных фильтров (обращение к полям и методам объекта одновременно в фильтре)
— вычислимые поля и индексы по ним
— композитные поля (хранимые в нескольких полях таблицы)
— поле вложенного объекта (поле, представляющее собой обычный объект python)
— связывание объектов из разных БД

Ну и конечно, хотелось бы видеть целостный framework для создания приложений, использующий pony ORM как основу для эффективного доступа к БД.

update 2013-08-03



Если вы хотите получить ответы авторов Pony ORM на свои вопросы, вы можете связаться с ними по следующим адресам: alexander.kozlovsky@gmail.com и m.alexey@gmail.com. Инвайты приветствуются.

Приложения



Результаты проведенных тестов


>>> import test_native
>>> test_native.test_native()
get row by key: native req/seq: 3050.80815908 req time (ms): 0.327782
get value by key: native req/seq: 4956.05711955 req time (ms): 0.2017733


>>> import test_django
>>> test_django.test_django()
get object by key: django req/seq: 587.58369836 req time (ms): 1.7018852
get value by key: django req/seq: 779.4622303 req time (ms): 1.2829358


>>> import test_alchemy
>>> test_alchemy.test_alchemy()
get object by key: alchemy req/seq: 317.002465265 req time (ms): 3.1545496
get value by key: alchemy req/seq: 1827.75593609 req time (ms): 0.547119


>>> import test_pony
>>> test_pony.test_pony()
get object by key: pony req/seq: 1571.18299553 req time (ms): 0.6364631
get value by key: pony req/seq: 2916.85249448 req time (ms): 0.3428353


Код тестов


test_native.py

import datetime

def test_native():
    from django.db import connection, transaction
    cursor = connection.cursor()

    t1 = datetime.datetime.now()
    for i in range(10000):
        cursor.execute("select username,first_name,last_name,email,password,is_staff,is_active,is_superuser,last_login,date_joined from auth_user where id=%s limit 1" % (i+1))
        f = cursor.fetchone()
        u = f[0]
    t2 = datetime.datetime.now()
    print "get row by key: native req/seq:",10000/(t2-t1).total_seconds(),'req time (ms):',(t2-t1).total_seconds()/10.

    t1 = datetime.datetime.now()
    for i in range(10000):
        cursor.execute("select username from auth_user where id=%s limit 1" % (i+1))
        f = cursor.fetchone()
        u = f[0][0]
    t2 = datetime.datetime.now()
    print "get value by key: native req/seq:",10000/(t2-t1).total_seconds(),'req time (ms):',(t2-t1).total_seconds()/10.


test_django.py

import datetime

from django.contrib.auth.models import User

def test_django():
   t1 = datetime.datetime.now()
   q = User.objects.all()
   for i in range(10000):
       u = q.get(id=i+1)
   t2 = datetime.datetime.now()
   print "get object by key: django req/seq:",10000/(t2-t1).total_seconds(),'req time (ms):',(t2-t1).total_seconds()/10.

   t1 = datetime.datetime.now()
   q = User.objects.all().values('username')
   for i in range(10000):
       u = q.get(id=i+1)['username']
   t2 = datetime.datetime.now()
   print "get value by key: django req/seq:",10000/(t2-t1).total_seconds(),'req time (ms):',(t2-t1).total_seconds()/10.


test_alchemy.py

import datetime

from sqlalchemy import *
from sqlalchemy.orm.session import Session as ASession
from sqlalchemy.ext.declarative import declarative_base

query_cache = {}
engine = create_engine('mysql://testorm:testorm@127.0.0.1/testorm', execution_options={'compiled_cache':query_cache})
session = ASession(bind=engine)

Base = declarative_base(engine)
class AUser(Base):
    __tablename__ = 'auth_user'
    id = Column(Integer, primary_key=True)
    username =  Column(String(50))
    password = Column(String(128))
    last_login = Column(DateTime())
    first_name = Column(String(30))
    last_name = Column(String(30))
    email = Column(String(30))
    is_staff = Column(Boolean())
    is_active = Column(Boolean())
    date_joined = Column(DateTime())

def test_alchemy():
   t1 = datetime.datetime.now()
   for i in range(10000):
       u = session.query(AUser).filter(AUser.id==i+1)[0]
   t2 = datetime.datetime.now()
   print "get object by key: alchemy req/seq:",10000/(t2-t1).total_seconds(),'req time (ms):',(t2-t1).total_seconds()/10.

   table = AUser.__table__
   sel = select(['username'],from_obj=table,limit=1,whereclause=table.c.id==bindparam('ident'))

   t1 = datetime.datetime.now()
   for i in range(10000):
       u = sel.execute(ident=i+1).first()['username']
   t2 = datetime.datetime.now()
   print "get value by key: alchemy req/seq:",10000/(t2-t1).total_seconds(),'req time (ms):',(t2-t1).total_seconds()/10.


test_pony.py

import datetime
from datetime import date, time

from pony import *
from pony.orm import *

db = Database('mysql', db='testorm', user='testorm', passwd='testorm')

class PUser(db.Entity):
    _table_ = 'auth_user'
    id = PrimaryKey(int, auto=True)
    username =  Required(str)
    password = Optional(str)
    last_login = Required(date)
    first_name = Optional(str)
    last_name = Optional(str)
    email = Optional(str)
    is_staff = Optional(bool)
    is_active = Optional(bool)
    date_joined = Optional(date)

db.generate_mapping(create_tables=False)

def test_pony():
    t1 = datetime.datetime.now()
    with db_session:
        for i in range(10000):
            u = select(u for u in PUser if u.id==i+1)[:1][0]
    t2 = datetime.datetime.now()
    print "get object by key: pony req/seq:",10000/(t2-t1).total_seconds(),'req time (ms):',(t2-t1).total_seconds()/10.

    t1 = datetime.datetime.now()
    with db_session:
        for i in range(10000):
            u = select(u.username for u in PUser if u.id==i+1)[:1][0]
    t2 = datetime.datetime.now()
    print "get value by key: pony req/seq:",10000/(t2-t1).total_seconds(),'req time (ms):',(t2-t1).total_seconds()/10.
Всеволод Новиков @nnseva
карма
33,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (18)

  • +2
    А запросы к субд при этом какие происходят?
    • 0
      Запросы идентичные с точностью до последовательности полей — разумеется, я это проверял в первую очередь, но оставил за пределами поста, чтобы его не загромождать.
      • +2
        > from django.contrib.auth.models import User

        Почему вы используете стандартный класс? Мне, например, не очевидно, что джанга не делает что-нибудь этакое в этом классе после получения данных из базы, что может замедлить работу теста. Предлагаю убрать магию и написать модель User с нуля для джанго-тестов.
        • 0
          «Стандартный» класс никак не отличается от любых других, тем более что входит в contrib, а не в core. Сделал я это лишь чтобы не отвлекать внимание на детали создания таблицы. «Этакое» джанга видимо делает со всеми классами — я то начал тесты не потому, что испытывал проблемы со стандартными классами, а как раз наоборот — потому что проблемы были с моими собственными.

          Конечно, для чистоты эксперимента вероятно нужно было бы сделать совсем пустой (только объявления полей) тестовый класс. Я подумываю о более широком обзоре скорострельности различных операций в различных ORM, там наверно сделаю именно так.
      • +2
        > Запросы идентичные с точностью до последовательности полей

        Как минимум, вы забыли, поле is_superuser в sqlalchemy и pony тестах. Я бы вообще предложил создать таблицу руками и замапить на неё модели фрймворков. Джанга и sqlalchemy это умеют, про пони не в курсе.
        • 0
          и вправду, забыл :( тем не менее, на получение значения username это никак не влияет, а на получение объекта — по минимуму. Можете проверить.
  • +1
    Хотелось бы увидеть тут и peewee
    • 0
      Будет время — посмотрю и его.
  • 0
    А на стороне БД нельзя настроить правильное кеширование?
    • 0
      В предложенных условиях, БД отрабатывает настолько оптимально, насколько это вообще возможно. В своем первом посте я вроде бы упоминал, что неоптимизированный код нашего приложения приводил к тому, что нагрузка на процессор со стороны нашего кода была в несколько раз выше, чем нагрузка со стороны СУБД. После перевода наиболее критических запросов на чистый SQL, нагрузка стала распределяться примерно поровну.
  • 0
    Очень хорошее начинание. Может, стоит выложить на БитБакет, например? ORM — большая головная боль с т.з. производительности, но Алхимия — ещё хуже. Хотел бы увидеть, как ваш код работает и насколько он удобен.
  • 0
    Ладно производительность, но вот в деле «трансляции Python выражений в SQL» они конечно всех превзошли…
    select(p for p in Person if 'o' in p.name)
    
  • +1
    Все ORM будут всегда тормозить. Надо построить нечто «компилируемое» в код Python в зависимости типа базы данных и прочее, что позволит без лишних операций и тысяч уровней абстракций производить необходимые действия. Фактический получится RAW SQL с человечным интерфейсом.
    • 0
      Вы имеете ввиду
      простую компиляцию, из серии «а ассемблер быстрее», с чистым SQL-кодом на выходе,
      или препроцессинг исходников перед деплоем на продакшн,
      или некий построитель запросов в IDEна подобии как это в 1С сделано?
      • 0
        Я имею ввиду кодо-генератор. На подобие Protobuf. Вы описываете необходимые структуры/схему базы данных, а «компилятор» генерирует код который работает с такой БД. Фактически получая статический код, который отсылает прямые запросы на сервер без лишних телодвижений построения сложных запросов ибо уже все построено. Само собой это накладывает ограничение на гибкость, но ничто не запрещает добавить необходимый набор «инструкций» описывающих необходимые операции.
    • 0
      Ну вот за счет многочисленных кешей компилированных и полукомпилированных запросов, pony имхо как раз ближе всего оказалась именно к такому подходу.
      • 0
        Ближе, но всеравно логики не мало в целом. Если потребуется массивная выборка данных, по несколько тысяч записей, создание тысяч микро объектов займет время. Выборка ненужных данных также займет не мало времени. Как возможный частичный выход попробуйте генерировать код классов с объявленным __slots__, что снизит накладные расходы на инстанцирование.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.