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

в 22:30, , рубрики: django, orm, python, высокая производительность, производительность, метки: , , ,

В своей первой статье на Хабрахабре я писал об одной из основных проблем существующих 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 как основу для эффективного доступа к БД.

Приложения

Результаты проведенных тестов
>>> 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

Источник

Поделиться

* - обязательные к заполнению поля