Откуда тормоза в ORM?

Анализ некоторых python ORM на непроизводительные расходы

Введение


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

Последующий анализ показал, что действительно, основным непроизводительным потребителем ресурсов процессора оказался django ORM, который был использован для доступа к данным, необходимым при расчетах.
Заинтересовавшись этим вопросом, я решил проверить, каковы непроизводительные расходы при использовании ORM. Для получения результата, я использовал самую элементарную операцию: получение username первого и единственного пользователя в только что созданной новой базе данных.

В качестве базы данных, использовался MySQL, расположенный на localhost (таблицы MyISAM).

В качестве исходного “примера для подражания”, я использовал код, минимально использующий специфику django и почти оптимально получающий необходимое значение:

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 from auth_user limit 1")
        f = cursor.fetchone()
        u = f[0][0]
    t2 = datetime.datetime.now()
    print "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,first_name,last_name,email,password,is_staff,is_active,is_superuser,last_login,date_joined from auth_user limit 1")
        f = cursor.fetchone()
        u = f[0][0]
    t2 = datetime.datetime.now()
    print "native (1) 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 = connection.cursor()
        cursor.execute("select username,first_name,last_name,email,password,is_staff,is_active,is_superuser,last_login,date_joined from auth_user limit 1")
        f = cursor.fetchone()
        u = f[0][0]
    t2 = datetime.datetime.now()
    print "native (2) req/seq:",10000/(t2-t1).total_seconds(),'req time (ms):',(t2-t1).total_seconds()/10.


Результат выполнения данного кода:

>>> test_native()
native req/seq: 8873.05935101 req time (ms): 0.1127007
native (1) req/seq: 5655.73751948 req time (ms): 0.1768116
native (2) req/seq: 3815.78751558 req time (ms): 0.2620691


Таким образом, оптимальный “образец” дает около 8 с половиной тысяч обращений к БД в секунду.
Обычно, django и другие ORM тянут за собой получение других атрибутов объекта при получении его из БД. Как легко заметить, получение “паровоза” из остальных полей таблицы довольно сильно ухудшило результат: до 5 с половиной тысяч запросов в секунду. Впрочем, это ухудшение относительно, поскольку часто для получения результата расчетов требуется более одного поля данных.
Довольно тяжелой оказалась операция получения нового курсора — она занимает около 0.1ms и ухудшает скорость выполнения кода почти в 1.5 раза.

Возьмем например второй результат за образец и посмотрим, какие потери добавляет ORM к полученным показателям.

Django ORM


Выполним несколько вариантов запросов, начиная от самых простых, и пытаясь последовательно использовать средства django для оптимизации запросов.
Вначале выполним самый неприхотливый код получения нужного атрибута, начав непосредственно с типа User.
Затем попробуем улучшить результат, предварительно сохранив объект запроса.
Затем вспомним об использовании метода only() и попробуем еще улучшить результат.
И наконец, еще один вариант попытки улучшить положение — использование метода values() который исключает необходимость создания целевого объекта.
Вот итоговый код, проверяющий результат наших усилий:
def test_django():
   t1 = datetime.datetime.now()
   for i in range(10000):
       u = User.objects.all()[0].username
   t2 = datetime.datetime.now()
   print "django req/seq:",10000/(t2-t1).total_seconds(),'req time (ms):',(t2-t1).total_seconds()/10.

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

   t1 = datetime.datetime.now()
   q = User.objects.all().only('username')
   for i in range(10000):
       u = q[0].username
   t2 = datetime.datetime.now()
   print "django (2) 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[0]['username']
   t2 = datetime.datetime.now()
   print "django (3) req/seq:",10000/(t2-t1).total_seconds(),'req time (ms):',(t2-t1).total_seconds()/10.

Результаты выполнения обескураживают:
>>> test_django()
django req/seq: 1106.3929598 req time (ms): 0.903838
django (1) req/seq: 1173.20686476 req time (ms): 0.8523646
django (2) req/seq: 695.949871009 req time (ms): 1.4368851
django (3) req/seq: 1383.74156246 req time (ms): 0.7226783

Во-первых, использование ORM само по себе ухудшило производительность более чем в 5 раз (!) по сравнению даже с неоптимальным “образцом”. Перенос подготовки запроса за пределы цикла не сильно (менее чем на 10%) улучшило результат. А вот использование only() и вовсе испортило картину — мы видим ухудшение результата почти в 2 раза, вместо ожидаемого улучшения. При этом, что интересно, исключение создания объекта помогло увеличить производительность на 20%.
Таким образом, django ORM дает увеличение непроизводительных расходов приблизительно на 0.7226783-0.1768116=0.5458667ms на получение одного объекта.
Опуская дальнейшие эксперименты, потребовавшие создания дополнительных объектов и таблиц, сообщаю, что данные результаты также верны и для получения списка объектов: получение каждого отдельного объекта коллекции объектов, которая является результатом выполнения одного запроса, приводит к потерям порядка полумиллисекунды и более на каждом объекте.
В случае использования MySQL, эти потери составляют замедление выполнения кода более чем в 5 раз.

SQLAlchemy


Для SQLAlchemy я создал класс AUser, декларативно объявляющий структуру данных, соответствующую стандартному классу django.contrib.auth.models.User.
Для достижения максимальной производительности, после вдумчивого чтения документации и некоторых экспериментов, был использован простой кэш запросов:
query_cache = {}
engine = create_engine('mysql://testalchemy:testalchemy@127.0.0.1/testalchemy', execution_options={'compiled_cache':query_cache})

Тестирование производительности выполняется сначала на “лобовом” варианте доступа к объекту.
Затем производится попытка оптимизации за счет выноса подготовки запроса за пределы цикла.
Затем производится попытка оптимизации за счет исключения создания целевого объекта.
Затем мы еще оптимизируем запрос, ограничивая набор запрашиваемых полей.
def test_alchemy():
   t1 = datetime.datetime.now()
   for i in range(10000):
       u = session.query(AUser)[0].username
   t2 = datetime.datetime.now()
   print "alchemy req/seq:",10000/(t2-t1).total_seconds(),'req time (ms):',(t2-t1).total_seconds()/10.

   q = session.query(AUser)
   t1 = datetime.datetime.now()
   for i in range(10000):
       u = q[0].username
   t2 = datetime.datetime.now()
   print "alchemy (2) req/seq:",10000/(t2-t1).total_seconds(),'req time (ms):',(t2-t1).total_seconds()/10.

   from sqlalchemy.sql import select
   table = AUser.__table__
   sel = select([table],limit=1)

   t1 = datetime.datetime.now()
   for i in range(10000):
       u = sel.execute().first()['username']
   t2 = datetime.datetime.now()
   print "alchemy (3) 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)

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

Вот результаты тестирования:
>>> test_alchemy()
alchemy req/seq: 512.719730527 req time (ms): 1.9503833
alchemy (2) req/seq: 526.34332554 req time (ms): 1.8999006
alchemy (3) req/seq: 1341.40897306 req time (ms): 0.7454848
alchemy (4) req/seq: 1995.34167532 req time (ms): 0.5011673

В первых двух случаях, алхимия не кешировала запросы, несмотря на их идентичность (я нашел — почему именно, но разработчики пока предлагают заглушить это какой-то затычкой, которую потом обещают воткнуть в код, я не стал этого делать). Кешированные запросы позволяют алхимии на 30%-35% превзойти django ORM по производительности.
Сразу отмечу, что SQL, сгенерированный django ORM и SQLAlchemy практически идентичный и вносит минимум искажений в тест.

ORM на коленке



Естественно, после таких результатов мы переделали весь наш код, получавший данные в алгоритмах обработки, на прямые запросы. Работать с кодом, осуществляющим прямые запросы, неудобно — поэтому мы обернули наиболее часто выполняемые операции в простенький класс, выполняющий задачи, сходные с ORM:

class S:
   def __init__(self,**kw):
       self.__dict__.update(kw)

   @classmethod
   def list_from_cursor(cls,cursor):
       return [cls(**dict(zip([col[0] for col in cursor.description],row))) for row in cursor.fetchall()]

   @classmethod
   def from_cursor(cls,cursor):
       row = cursor.fetchone()
       if row:
           return cls(**dict(zip([col[0] for col in cursor.description],row)))

   def __str__(self):
       return str(self.__dict__)

   def __repr__(self):
       return str(self)

   def __getitem__(self,ind):
       return getattr(self,ind)


Померяем потери производительности, вносимые использованием данного класса.

def test_S():
   from django.db import connection, transaction
   import util
   cursor = connection.cursor()
   t1 = datetime.datetime.now()
   for i in range(10000):
       cursor.execute("select * from auth_user limit 1")
       u = util.S.from_cursor(cursor).username
   t2 = datetime.datetime.now()
   print "S 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 limit 1")
       u = util.S.from_cursor(cursor).username
   t2 = datetime.datetime.now()
   print "S opt req/seq:",10000/(t2-t1).total_seconds(),'req time (ms):',(t2-t1).total_seconds()/10.

Результаты тестирования:
>>> test_S()
S req/seq: 4714.92835902 req time (ms): 0.2120923
S opt req/seq: 7473.3388636 req time (ms): 0.133809

Как видим, потери весьма скромны: 0.2120923-0.1768116=0.0352807ms в неоптимальном случае и 0.133809-0.1127007=0.0211083ms в оптимальном. Замечу, что в нашем ORM, сделанном на коленке, создается полноценный объект python.

Общий вывод


Использование мощных универсальных ORM приводит к очень заметным потерям производительности. В случае использования быстрых движков СУБД, таких как MySQL — производительность доступа к данным снижается более чем в 3-5 раз. Потери производительности составляют около 0.5ms и более на обращение к одному объекту на платформе Intel Pentium Dual CPU E2200 @ 2.20GHz.
Существенную часть потерь составляет создание объекта из строки данных, полученных из БД: примерно 0.1ms. Еще 0.1ms отъедает создание курсора, избавиться от которого в ORM довольно сложно.
Происхождение остальных потерь осталось неизвестным. Можно лишь предположить, что достаточно большой объем потерь может быть вызван количеством вызовов при обработке результата — за счет абстрагирования слоев обработки данных.

Для достижения адекватной производительности, разработчики ORM должны иметь в виду потери на прохождении кода слоев абстракции, на конструировании запроса и на других операциях, специфических для ORM. Действительно производительный ORM должен предусматривать возможность для разработчика, использующего этот ORM, единожды подготовить параметризованный запрос и затем использовать его с различными параметрами с минимальным влиянием на общую производительность. Одним из способов реализации такого подхода является использование некоторого кэша для сгенерированных выражений SQL и хендлов подготовленных запросов, специфических для нижележащей СУБД. К моему удивлению, несмотря на то, что такая оптимизация сделана в SQLAlchemy, производительность все равно страдает, хотя и несколько менее сильно.

Для меня лично — осталось загадкой, откуда берутся еще 0.3-0.4ms потерь при чтении одного объекта на обоих ORM. Характерным является то, что оба ORM практически одинаково непроизводительно тратят ресурсы процессора. Это заставляет думать, что потери вызваны не какими-то локальными проблемами ORM (как отсутствие кеша подготовленных запросов у django), а архитектурными элементами, которые вероятно одинаковы у обоих ORM. Буду благодарен сообществу за профессиональные комментарии.
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 63

    +17
    По-моему, некорректно считать разницу производительности в абсолютных единицах (мсек в данном случае). Эта информация бесполезна, т.к. очень сильно зависит от множества факторов и совершенно ни о чём не говорит. Надо сравнивать относительные величины (потеря производительности — 10%)
      –2
      Проценты от общей производительности в общем случае столь же некорректны, так как зависят от внешних условий — производительности СУБД например. Поэтому в разных местах — разные сравнения, и в разах, и в процентах, и в миллисекундах.
      Вопрос на самом деле в том, что применяя существующие ORMы, мы очень сильно теряем в производительности — настолько сильно, что применение ORM становится бессмысленным, поскольку приводит к неработоспособности приложения в условиях, когда оно — по объемам производимой полезной нагрузки — работать должно.
        +11
        Немного не понял мысль. Это очевидно, что ОРМ будет работать медленнее, чем нативные запросы, но
        применение ORM становится бессмысленным
        Почему бессмысленным? Зачем такие громкие заявления?
        приводит к неработоспособности приложения
        В каком месте? По-моему, очень даже наоборот, помогает быстрее и проще писать приложения. Кроме того одно из преимуществ ОРМ — это (теоретически) независимость от используемой БД
          –6
          Бессмысленно — именно из за производительности. Да, разумеется, можно ОЧЕНЬ быстро смастрячить приложение, которое будет прекрасно работать в тестовом режиме — на малых нагрузках. Однако его готовность к выпуску будет неявно сильно переоценена — так как для того, чтобы оно реально заработало на реальных данных — то есть стало работоспособным в той степени, которая необходима для выпуска приложения — нужно будет переписать условно 90% кода, который так замечательно быстро был написан на первой стадии.
          При этом, надо заметить — то что на первой стадии было понятно, просто и совместимо — станет сложным, непонятным и абсолютно несовместимым.
          И наконец — иллюзия быстроты написания приложения — а это получается именно иллюзией в силу вышеприведенных причин — приводит еще и к тому, что часть архитектуры приложения, завязанной поначалу на использование ОРМ, становится балластом, с которым приходится дополнительно бороться в процессе приведения приложения в состояние, пригодное для выпуска.
            +7
            Позвольте спросить, ваши заключения основаны на реальном опыте или сделаны на основании вышепреведённых в статье тестов? Лично я знаю несколько довольно крупных проектов (в некоторых из них принимал участие), которые используют ОРМ и очень неплохо себя чувствуют в продакшене
              +1
              А на чьем же еще? Я своими руками переписал узкий кусок кода, потратил на это месяц, получил прирост производительности в 5 раз и теперь смотрю на эту дикую смесь, хорошо понимая — изменись структура данных, и мне понадобится еще месяц на переписывание и отладку. При том, что сохрани я орм, на те же изменения у меня уйдет 2-3 дня.
              Вот и пытаюсь понять — сохранять ли мне мобильность кода, платя 5-кратными потерями на эксплуатации, или тратить уйму времени на ввод каждой новой фичи.
              Поэтому и пост написал — может кто что посоветует.
                +1
                Закон 80/20.
                Переписывайте только критические к скорости участки кода.
                В любом другом случае, будет дешевле купить железо, чем тратить средства на очень длительную разработку.
                  0
                  Посчитайте что стоит дешевле: месяц вашей работы или, грубо говоря, аренда ещё четырёх серверов. Для начала можно без «облачных» выкрутасов типа стоимость по фактическим ресурсам и/или автоматически запускаемых инстансов.
                    0
                    В случае с django ORM можно еще вот что попробовать:

                    — Если тормозит создание моделей User — не создавать их, использовать values или values_list.
                    — Если тормозит клонирование QuerySet — не использовать его или использовать по минимуму, вынося за циклы.
                    — Если тормозит построение запроса — поковыряться в исходниках и строить запрос отдельно и использовать метод raw (см. docs.djangoproject.com/en/dev/topics/db/sql/#performing-raw-queries ). При этом запрос можно все еще строить с помощью ORM через qs.query.sql_with_params(). Параметры потом менять, как нужно и если нужно, и передавать в User.objects.raw(). Если это в хитрую утилиту оформить, то и ORM сохранится, и месяц на отладку не понадобится. Я сам не пробовал, но мне кажется, что должно получиться.
          +3
          Тормоза в ОРМ от удобства. habrahabr.ru/post/140713/ вот тут я рассказывал о некоторых удобствах полученных нами.
          Моя практика говорит о том что ОРМ хорош, в случае если работа происходит с изменяемой структурой данных. В данных случаях проще заплатить в несколько раз больше за более производительный сервер, чем растягивать сроки решения задач или нанимать дополнительных программистов.
          Опять же для теста был задан очень простой запрос. А что будет на более сложном? как зависят типы данных, количество объектов, количество полей объектов?
          Может быть в ОРМ есть какой-нибудь логгер, который отнимает много времени?
          Не хотите провести более подробный анализ и поделиться с сообществом?
            –2
            Логгеры были отключены разумеется, DEBUG=False.
            Что касается количества полей — на ограничении количества запрашиваемых полей из приведенного примера хорошо видно, что и django ORM, и SQLAlchemy довольно много теряют на полях. Однако если вы обратили внимание, даже в примерах, в которых ORMы берут ровно одно поле данных, они проигрывают эквивалентному «голому» запросу в несколько раз.
            Это означает, что ORMы выполняют какую-то совершенно ненужную работу в случаях, когда эта работа никому не нужна.
            Я был готов заплатить за удобство ORMа понижением производительности условно говоря в 2 раза. Но не в 5 же раз! 5-1=4 хороших производительных сервера стОят поллимона рублей. За эти деньги можно нанять команду программеров, которые с нуля напишут производительный ORM или его подобие на C++ и прикрутят его к питону.
            Про удобства ORMа я и сам прекрасно знаю. Но реальность такова, что срок выпуска приложения в результате не приближается, а растягивается — поскольку борьба с его производительностью, вместе с мучительными выяснениями, на каком же слое происходят потери — с глубинным анализом чужого кода, изучением кучи документации и так далее — занимает в совокупности больше времени, чем написание лобового кода доступа к данным, пусть непрозрачного и абсолютно нерасширяемого.
            Анализировать я конечно буду еще — это рабочий проект, который мне нужно как-то выводить на рабочие рельсы.
              +4
              5-1=4 хороших производительных сервера стОят поллимона рублей. За эти деньги можно нанять команду программеров, которые с нуля напишут производительный ORM или его подобие на C++ и прикрутят его к питону.


              Это максимум 2 месяца работы 3 человек, никто так быстро шустрый ORM вам не напишет.
                +3
                Это еще раз говорит о том, что автор не совсем «в теме».
                  –2
                  Ну я может и погорячился конечно, но мне реально непонятно — неужели за то время пока алхимия живет — никто почти не думал о производительности?
                    +3
                    Алхимия и орм джанги универсальные штуки. Они работают с разными субд. В них мало оптимизаций под конкретные задачи и их трудно сделать одинаково эффективными для всего. Поэтому иногда оптимальней использовать raw sql, но это требуется (в моей практике) в очень редких случаях =)
                  +6
                  Простой запрос — мало времени БД и много ОРМ.
                  Представим, что вы выполняете запрос «SELECT 1+1» и сравниваете его с простым вычислением 1+1
                  Окажется что работа с БД супер медленная штука, что выполнение 1+1 средствами самого ЯП на несколько порядков превосходит по времени время выполнения через БД. например БД (через ОРМ это делает 1 мс, а ЯП 1/1000 мс)
                  Но возьмем 10 таблиц, по 1-10М записей, составим запрос с несколькими JOIN и GROUP BY, UNION и т.д.
                  Окажется что реализованный вами алгоритм на ЯП работает не так быстро как хотелось бы, потом вы разбираетесь в узких местах, оптимизируете и оказывается что ваш вариант на 5 мс быстрее!
                  Круто же! в предыдущем примере было около 1мс, а тут целых 5 сэкономили.
                  Но само время выполнения запроса уже 100мс, и на этом фоне разница в 5 мс выглядит не существенной.
                    –2
                    Проблема в том, что когда я использую django ORM — у меня почти нет шансов получить все данные, которые мне нужны, одним запросом. При том, что такой запрос на голом SQL я напишу. И половину обработки данных туда же засуну. И получу то, о чем вы говорите.
                    А когда ORM берется добывать для меня объекты из базы ПО ОЧЕРЕДИ — вот тут то и возникают тормоза, поскольку на добывание КАЖДОГО из этих отдельных (с точки зрения ORM) объектов — уходят пресловутые полмиллисекунды.
                    И даже использовав связку django+aldjemy+SQLAlchemy (которая дает мне надежду, что все не так плохо и я смогу сконструировать нужный мне супер-пупер-мега-запрос, пользуясь чисто фишками алхимии), пока план запроса остается в рамках ссылок по ключам, останется вопрос, куда же все-таки девается почти полмиллисекунды при получении КАЖДОЙ строки данных — потому что сам-то запрос будет тратить время в пределах той же одной десятой миллисекунды на строку, а то и существенно меньше, если я получаю много строк. Не говоря уже о нагрузке на проц, который не безразмерен.

                    Вот наблюдаемый мной результат: до оптимизации нагрузка при выполнении обработки данных распределялась как 5% ядра на MySQL и 80%-90% ядра на python. После оптимизации (перехода на чистый SQL) я наблюдаю 50% на 50% при увеличении производительности в 5 раз (примерно с 200 до 1000 обработанных приложением элементов приходящих данных в секунду), как я уже говорил. Последняя цифра говорит мне, что примерно половину времени обработки python стоит, дожидаясь доставки данных из MySQL — что не так уж и плохо. Но это в свою очередь означает, что MySQL так и не стал узким местом — таким образом, нагрузив питоновскую часть даже относительно производительной алхимией, я получу существенное снижение производительности.
              • UFO just landed and posted this here
                  0
                  Это не совсем веб. Веб там интерфейс, а еще нужно анализировать данные с сотни тысяч приборов по 3 — 4 записи в мин с каждого. Приложение почти реал тайм.
                  • UFO just landed and posted this here
                    –1
                    Вообще, такие бывают. Отображение графиков, например (статистика). Там, правда, с большой вероятностью может быть работа с нереляционной БД (MongoDB, например), но тем не менее.

                    Либо отображение точек на карте. Либо поиск чего-либо.
                    • UFO just landed and posted this here
                        0
                        А, вы имеете в виду именно тысячи отдельных запросов к БД? Тогда да, таких случаев действительно мало, и скорее всего это означает, что либо архитектура приложений не продуманная, либо ORM (или сам человек) неправильно строит запросы, либо программист не умеет пользоваться данным ORM.
                        • UFO just landed and posted this here
                          0
                          О, кстати, про вычислительную сложность — вспомнился курс по базам данных Ильи Тетерина.
                      0
                      Да ладно, разница минимальная. Лучше взять сервер по мощнее, чем писать и дебажить кучу sql запросов.
                        –6
                        Тема статьи очень интересна.
                        Но написано очень тяжело.
                        Очень.
                        Я как раз в эту минуту прикручиваю КЛАДР в django (~1 млн записей) — и прямо в тему — но статью читать очень тяжело — «многабукаф».
                        PS. за корректность теста пока молчу.
                          +2
                          Добавьте пожалуйста sql-профилирование, тогда можно будет о чем то говорить. Техника использования ORM и SQL запросов, может отличаться в узких местах, в профилировании мы это увидим.
                          Вообще, с выводами в явно поспешили, если автоматом пытаться пользоваться как дубинкой, тоже наверно не лучшие характеристики будут.
                            –3
                            Какое профилирование простите вам нужно? Голая база, в которой ровно одна запись в одной таблице пользователей. Запросы почти идентичны во всех четырех рассмотренных случаях, в двух из четырех эти запросы напрямую приведены в статье.
                            Добавлю еще, что специфика MySQL такова, что его производительность очень медленно снижается при увеличении количества записей в таблицах (ну пока на индексы с таблицами мозга хватает, разумеется) — поэтому на результатах тестирования почти никак не сказывается, одна запись в таблице или сотня тысяч.
                            Еще добавлю, что запросы, подобные приведенному (с поправкой на поиск по первичному ключу) — очень характерны при использовании ORM, особенно django ORM. По сути, невинное обращение по атрибуту, соответствующему foreign key, выполняет почти такой же запрос. С соответствующими, описанными в статье, потерями.
                            Косвенным подтверждением выводов статьи явилось для меня то, что переписывание критической части кода приложения на прямое обращение к SQL дало нам как раз тот самый прирост производительности в 5 раз.
                            В результате получается, что потери на SQL ничтожны по сравнению с потерями на ORM.
                              0
                              погуглите, чтоли
                                +1
                                вы как-то забываете еще о присутствии курсоров, пуллинге соединений с накладывающимися проверками на привышение лимитов, компилятор sql диалекта в алхимии (ведь ваш код будет работать как минимум на 5-х БД-бэкендах, без изменений), вообще там под капотом от объявления declarative_base и class-mapper до реального запроса к бд лежит довольно внушительный слой кода позволяющий конечному программисту меньше думать об особенности БД и больше о предметной области и взаимодействии сущностей.

                                Плюс — вы не указали, использовалась алхимия с акселераторами или без. Django-ORM таковых не имеет, поэтому сравнение производительности этих двух решений совершенно некорретно, на мой взгляд.

                                Пишите хранимые процедуры, не тратьте время на синтетические тесты.
                                  –1
                                  Про акселераторы я уточню, спасибо за наводку. Вообще, я ставил sudo pip install sqlalchemy и больше никаких телодвижений не предпринимал, подразумевая что акселераторы должны завестись автоматически.
                                  Хранимых процедур, как и всего остального специфического для SQL, мне очень хочется избежать, поскольку я не исключаю смены платформы SQL.
                                  Про курсоры я не забываю, потеря времени на создание курсора составила около одной десятой миллисекунды, это вообще-то говоря больше, чем мне бы хотелось.
                                  В джанге соединение насколько я понял, заводится одно на тред. Что касается алхимии, посмотрю, какие там есть политики.
                                  Про компилятор и остальной «внушительный слой» я вот как раз и думаю, как бы закешировать ВСЮ предварительную обработку? В алхимии кешируется подготовленный запрос — я использовал кеш запросов и получил прирост почти в 3 раза от исходной производительности (с 500 до 1400 запросов в сек), но максимум-то 1400 запросов, а не 3800 получаемых мной на голом SQL даже с созданием курсора на каждом обороте. Без пересоздания — эти данные не вошли — там было 5400 оборотов в сек.
                                    0
                                    А если обойтись без класс-маппера? Сделать всё на Table, Column и т.п? Чувствую, что оверхеда должно быть меньше, т.к. маппер притягивает метакласс для обработки атрибутов класса/экземпляра, перекрывает __init__ класса блокирует поток для создания реестра смапленных классов и делает еще кучу всего
                                      0
                                      Вот вариант «alchemy (3)» как раз почти ровно то самое и делает, о чем вы и говорите, и показывает 1400 оборотов в сек. То есть он берет единожды созданную Table из маппера и ее использует.

                                      Мне не удалось заставить класс-маппер использовать кеш компилированных запросов, поэтому результаты «alchemy (1)» и «alchemy (2)» такие унылые.

                                      Насчет кеша запросов — разработчики где-то в недрах гугл-группы рекомендовали использовать некую заглушку для класс-маппера, я в ней до конца не разобрался и пока не прикручивал.
                                    0
                                    Про акселераторы уточнил. Алхимия на момент тестов была с акселераторами и они были активны.
                                0
                                Помню я когда-то давно изучал один ORM на С++. Он был сделан на шаблонах и разумеется имел статическую типизацию, это ведь С++… Знаете какую задержку он привносил в работу программы? Никакую. Разница в замерах с прямой работой с SQL находилась в пределах погрешности измерений.
                                Я всегда этому поражался. Десятки, а может сотни отличнейших программистов пишут ОС, работу файловой системы, квантование времени процессора, пишут базы данных, которые ухитряются делать кучу разных вещей, обращяются к диску, к кешу, ведут транзакции, делают индексы, контролируют целостность данных, пересылают это все по сети используя кучу драйверов и промежуточных звеньев, но это все фигня! Потому что потом приходит умник и пишет крутую фичу как обернуть это все в объект. И реально эта одна операция занимает у него в 10 раз больше времени, чем все те сотни и тысячи операций которые тщательно выверяли и выписывали до него. Он реально берет микроскоп и лупит им по гвоздям! Как, черт, как они это делают?
                                И при том Питон вообще довольно быстрая штука. Вы удивитесь какую скорость он может давать если выкинуть из программы ORM и т.п. «улучшайзеры». На реальных задачах которые активно используют БД, разница может быть в 10-30 раз. (facepalm)
                                  +3
                                  найдите мне 15 человек с адекватным рейтом для проекта со сроками в 1.5 года, на платформу OpenStack которые после отказа от «улучшайзеров» не проклянут меня на чем свет стоит.
                                    –2
                                    А я и не говорю что «улучшайзеры» — зло. Мне лишь интересно посмотреть кто и как расчудесным образом писал конкретно эти ORM. Почему в десятки раз более сложные процессы в ядре ОС и базе данных выполняются в сотни раз быстрее? Сложно базу данных в объектную модель завернуть? Чай не нанотехнологии.
                                    Беда не в самой идее, а в том как она написана. И почему этот трагический перекос в качетсве реализаций разных систем, которые при этом претендуют на звание зрелых и популярных.
                                      0
                                      SQLAlchemy — Michael Bayer, претензии можно отрпавить на mike[at]zzzcomputing.com
                                      Django-ORM — Alex Gaynor, Adrian Holovaty и компания. За конкретикой и критикой — на гитхаб.
                                        0
                                        Потому что ORM пишется на интерпретируемых языках с без статической типизации. И в этом есть смысл.
                                        Реальность устроена так, что проще увеличить в 100 раз производительность серверов и писать на python/ruby/javascript, чем нанимать в 100 раз больше программистов для написания серверного кода за то же время на C. И даже после того, как код написан наступает проблема его поддержки, отладки, модернизации, масштабировании и т.п.

                                        Это логика эволюции: машинный код → ASM → C/C++ → интерпретируемые языки →?
                                        Между прочим, если посмотреть например OPA, то в ней JavaScript фактически используется как ассемблер :) Она в него компилируется. И судя по примеру микро-википедии на 60 строк ОПА-кода — это явно привет из будущего.
                                      0
                                      Рискну на оффтоп. Я могу совершенно точно сказать, как получается хороший и плохой код. Возьмите программиста и усадите его на тачку с 4 ядрами и 4 гигами. Поручите написать код. Потом усадите его на жалкийт пент 900мгц с одним ядром и полугигом мозга. И поручите ту же задачу. Уверяю — качество кода во 2м случае будет несравненно лучше.
                                        +1
                                        Вот это очень врят-ли. У меня программисты сидят на восмиядерниках с 16 — 32 гб оперативки, и при этом они выверяют каждую инструкцию в своих программах. Постоянные тестирования производительности и целостности программ (у нас серверный софт). И разумеется некоторое количество специально выделенных реальных и виртуальных тестовых машин. Например те коллизии, которые случаются на многопроцессорных мощьных машинах, на слабых могут и не проявиться никогда. Мощьный процессор не значит что его можно тратитьна что попало, потому что потом клиенты у тебя спросят — почему у тебя софт в 5 раз медленнее чем у конкурентов?
                                        А умелец на 5-м пне как раз скорее понапишет говнокода. Это же нужно себя не уважать, чтобы работать за такой машиной.
                                      +7
                                      По-моему самый обычный trade-off: удобство при разработке и поддержке взамен на производительность.
                                      Вцелом, любой мейнстримовый ORM это довольно сложная система, основная цель которой — создание абстракции, которая одинаково хорошо работает с разными бд. И производительность, к сожалению, из-за этого условия часто страдает.
                                        –2
                                        Ну вот когда-то я написал на C аналог strcpy и получил тормоза в 2 раза. Там проблема была в том, что ассемблерный strcpy использовал loop, а эту команду иначе чем использованием switch из C не сгенерить. Ну если не вставками, разумеется. А любой цикл в C генерил мне jmp/jne или что-то наподобие. Что на тех платформах (8086) составляло в несколько раз больше времени чем loop (насколько я понимаю, сейчас это не актуально).
                                        Это я понимаю. Однако очень хорошо помню, что первоначально написав тот код, я потерял не в 2, а в 4 раза. На чем? На ширине слова. Стандартный strcpy копировал по слову, в то время как мой первый код копировал побайнтно.
                                        Вот здесь ровно то же самое. Я готов мириться с потерями на абстракцию в 2 раза. А в 5 — имхо многовато.
                                          +2
                                          Ну это уже чисто психологическое: в 2 раза, в 5 раз) Абстракция может что-то тормозить на 10% и поэтому не подходить для задачи, а может тормозить в 100 раз и подходить при этом прекрасно. Меня не парит, что цикл в питоне в сотню раз медленнее, чем в C, т.к. обычно это не проблема, а узкое место можно при необходимости и на C переписать. Так и скорость ORM в вебе обычно тоже не проблема.

                                          Впрочем, это не означает, что разбираться в инструментах и улучшать их не нужно. Так что за бенчмарки плюс. Ну и обычно так бывает: если не знать, как твои инструменты работают на уровень-два ниже, а относиться к ним исключительно как к черным ящикам, ерунду можно написать, т.к. абстракции текут. Это легкий троллинг по поводу рассуждений об «архитектурных элементах» — взять профайлер, почитать исходники да разобраться, в чем конкретно дело, вот и решится загадка.
                                            –1
                                            Меня интересовало нет ли чего-то еще, что я мог упустить. Вот проверю еще раз акселераторы, которые упомянул nimnull, и подумаю крепко, нельзя ли сбоку прикрутить к алхимии кеш всего что там накручено. И взгляну еще на эликсир, он вроде бы не совсем сверху, а как-то наискосок к алхимии приделан, может там чего полезное увижу.
                                            А что касаемо архитектурных элементов — лет 5 назад широко обсуждалась тема, что ормы вообще принципиально ухудшают производительность. Не просто потому что добавляют абстракции и соответствующий рантайм, а потому что как раз таки архитектурно с скулем несовместны. И где-то как-то я готов согласиться хотя бы исходя из практики, поскольку нормального орма ни одного так и не увидел за всю свою жизнь.
                                        +3
                                        Не пробовали замерять скорость сложения 1000000 числе в программах на С с ассемблерными вставками и на том же питоне?
                                          0
                                          см выше ответ Prophet. Там почти про то же самое.
                                          +2
                                          А такой вопрос — где конкретно ORM кушает ваши драгоценные тысячи запросов в секунду?
                                          Попробуйте сгенерировать запросы алхимией например, а выполнить без привлечения мапперов и прочей абстракции. У меня есть подозрения, что все станет гораздо лучше — запросы будут описаны гибко, а скорость та же.
                                          То есть эту рекомендацию я строю из предположения, что тратится время на маппинг, перетасовку результатов.
                                            0
                                            Так об том и речь. test_native() (первый кусок кода) именно это и делает — берет РОВНО такой же, что и сгенеренный запрос и выполняет его безо всяких мапперов и остальных слоев.

                                            Конкретные затраты: на собственно запрос и получение данных в libmysql уходит ~0.1ms, на заворачивание в «объект» ~0.1ms, на лишнее создание курсора — еще ~0.1ms.

                                            Остаются еще ~0.3-0.5ms — непонятно на что. Неужто промежуточные слои так неэффективны?
                                              0
                                              Ну ваши объекты и объекты алхимии и джанго несколько разного калибра скорее всего.
                                              А вообще точный ответ на вопрос где именно тормозит, вам даст профайлер.
                                                +1
                                                Да, хотя бы банальнейший «python -m cProfile orm_tests.py» может дать многое, а если ещё сохранить результаты в файл и потом поковыряться в нём… глядишь, и найдётся место, где происходят основные задержки.
                                            +9
                                            Можно попробовать разобраться, что где происходит.

                                            В django ORM явные кандидаты на тормоза — постоянные копирования QuerySet (которые нужны для модной ленивости запросов) + сигналы post_init для полей (которые нужны для модных полей вроде ImageField или GFK — но платят за это все, если в проекте хоть где-то есть хоть одна модель с ImageField или GFK) + генератор SQL на объектах (что нужно для поддержки всех баз данных и сложных запросов).

                                            Что происходит:

                                            1) qs = User.objects.all()

                                            Метод .all() создает экземпляр QuerySet. QuerySet создает экземпляр Query. Экземпляр Query — достаточно большой питоний объект, у него где-то 50 полей (словарей, tuple, экземпляров других классов), создающихся в конструкторе.

                                            2) qs[0]

                                            При слайсинге сначала создается новый QuerySet (копируется исходный — в.т.ч. все из Query); у query выставляются новые лимиты: от [0, 1).

                                            Затем по Query (который к базе не привязан) db.mysql.compiler.SQLCompiler собирает запрос для mysql (там всяческа логика на основе тех 50 полей из query).

                                            Т.е. в тесте построение запроса и копирование QuerySet за цикл, можно сказать, не вынесено.

                                            После этого запрос выполняется и для полученного результата строится экземпляр джанго-модели User, в процессе чего шлются сигналы pre_init и post_init. Если в проекте где-то используются ImageField или GFK (например, django.contrib.auth GFK используется), то у этих сигналов есть слушатели, «короткий» путь не работает, и при каждой отправке сигнала для всех слушателей проверяется, живы ли weakref-ы и тот ли sender.

                                            .values() вдобавок к .all() убирает издержки по созданию экземпляра User (кстати, лучше просто написать было User.objects.values('username')). Для чистоты эксперимента можно еще User.objects.values_list('username', flat=True) попробовать, чтоб убрать влияние получения лишних данных.

                                            Что где конкретно тормозит — профайлер покажет)

                                            Но можно немного и без профайлера порассуждать. Т.к. values() вместо all() убирает издержки по созданию экземпляров User, то тормозит, наверное, постоянное копирование QuerySet и/или сбор Query в конкретный запрос под mysql.

                                            Можно попробовать провернуть какой-нибудь хак и убрать копирование QuerySet/Query, чтоб проверить, тормозит это копирование или построение sql-запроса для бэкенда (код не проверял!):

                                            qs = User.objects.values_list('username', flat=True)
                                            qs.query.set_limits(0, 1) # получаем элементы из полудиапазона [0; 1)
                                            
                                            for i in range(10000):
                                                user = list(qs.iterator())[0] # iterator нужен, чтоб значения из кэша не брались
                                            


                                            Если провести другой тест, в котором будет получаться 10000 результатов из одного запроса, проверяться будет скорость построения джанго-моделей (там values/values_list должен здорово помочь), т.к. sql строиться 1 раз должен будет.

                                            Но точный ответ, понятное дело, только профайлер даст — узкое место всегда оказывается не там, где думаешь :)
                                              0
                                              О, отлично, спасибо за наводку. Нет, проект полностью пустой, ни одного приложения кроме джанговского стандартного стартового набора.
                                              И да, сбор в запрос просто таки обязан тормозить.
                                              +6
                                              Я потратил немного времени, потестировал с джанго-орм, вот мои итоги:
                                              1. Для джанго орма, нет разницы между 1 и 2 тестом: запрос выполняется и там и там,
                                              2. Профилирование я все таки включил, видно что 3 тест с only, с точки зрения sql эквивалентен с 1 из test_native()
                                              3. Если посмотреть исходники orm, ясно что время которое джанго есть время на построение обслуживающих генераторов, инициализацию классов Query, Queryset, вызова маппера. Отсюда вывод что с ростом сложности запроса это время не будет меняться, т.е. НЕ УВЕЛИЧИВАЕТ В 5 РАЗ, А УВЕЛИЧИВАЕТ НА 0.05 — 0.1ms

                                              Но Вы когда начали пользоваться интерпретатором тоже согласились, на что он будет медленнее, теперь вы тоже согласиться что при использование хитро заточенных орм, вы платите производительностью. Но не все так плохо, потому для большинства web приложений работающих с БД: +0.1-0.2ms не так критично, более того это не защищает многих от выстрелов в ноги. Если Вам нужно ответ порядка ~0.1-2-3 ms, поверьте работа на прямую с бд вас не спасет. Вам могут помочь только висящие в памяти хранилища, типа memcached и redis.

                                              С моей точки зрения, ваш пост опасен для тех, кто только знакомится с программированием, потому что Вы не разобрались до конца — все же откуда (не читали исходников), не прогнали разных тестов ( с большим кол-вом записей, с работой с удаленной БД), не задали вопроса где-нибудь на stackoverflow и т.п.

                                                0
                                                Вот по ссылочке ниже — большой пост из django-dev, цитата:

                                                The ORM currently uses 4x the time in Python compared to the time the DB needs to parse, plan and
                                                execute a query like this: Model.objects.get(pk=1).

                                                Anssi Kääriäinen

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

                                                Исходя из статьи вывод можно сделать пока только такой: Использование Django ORM приводит к заметным потерям.
                                                Ждем анализа остальных ORM.
                                                  +1
                                                  Простите, но SQLAlchemy мне конечно удалось заставить аж на 30% джангу обогнать… но только после того, как я заюзал кеш (скомпилированных) запросов (и то, маппер мне заставить его использовать так и не удалось, поэтому результаты получены только для случая, когда я добываю конкретные значения).

                                                  До использования кеша SQLAlchemy отставал от джанги на 30%-50%. Обратите внимание на результаты:

                                                  django req/seq: 1106.3929598 req time (ms): 0.903838
                                                  django (1) req/seq: 1173.20686476 req time (ms): 0.8523646
                                                  
                                                  alchemy req/seq: 512.719730527 req time (ms): 1.9503833
                                                  alchemy (2) req/seq: 526.34332554 req time (ms): 1.8999006
                                                  


                                                  Первые два — алхимия с маппером вида session.query(AUser)…
                                                  Вторые два — джанго вида User.objects.all()…

                                                  Функциональность приведенных тестов — идентичная, поскольку джанго изначально ничего не кеширует, ибо не умеет, а у алхимии кеш запросов на маппере без бубна не заводится.

                                                  Вот и думается — что лучше, кеш компилированных запросов к джанге подвешивать, или искать тормоза в алхимии? Мне лично алхимия больше нравится, ибо гибче. Но джанга зело соблазняет — проще в использовании и в сравнимых условиях — производительнее.
                                                    0
                                                    А кеш запросов в джанге, если память не изменяет, планируется то ли в 1.5, то ли в 1.6
                                                  0
                                                  сколь бы ни был хорош django orm, как прикладное api, в реализации это куча тормозного говнокода. факт.

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

                                                  не так давно, некто Anssi Kääriäinen, в dev-рассылке предлагал заняться поэтапным рефакторингом, и взять для начала — QuerySet. тут естественно возникла дилемма: свежая кровь и здравый смысл vs совместимость с легаси-кодом (ааа. кабыче у кого не сломалось). подробности по ссылке, есть-ли прогресс — не знаю.

                                                  альтернативная мысль: давайте использовать SQLAlchemy, как все нормальные люди, — это комета Галлея. прилетает, сверкает, делает много шума, а затем опять погода — тишина. :)

                                                  в общем, спасибо за статью, но если действительно есть желание разобраться с проблемой, и хватает скиллов, — айда в django-dev@ :)
                                                    0
                                                    > сколь бы ни был хорош django orm, как прикладное api, в реализации это куча тормозного говнокода. факт.

                                                    django orm, как прикладное апи — тоже параша. :) Он такой же клëвый, как и быстрый.
                                                    +2
                                                    > архитектурными элементами, которые вероятно одинаковы у обоих ORM.

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

                                                    Но вообще, топики «ормы говно» — это уныло. Тысячи их и лень вообще что-то писать.
                                                      0
                                                      Смотрели peewee? Когда тестировал была много быстрее джанги и алхимии.

                                                      Only users with full accounts can post comments. Log in, please.