Подтвердить что ты не робот

Лучше понять SQLalchemy `yield_per()` проблемы

Чтобы привести документацию SQLalchemy:

Метод Query.yield_per() несовместим с большинством активных схем загрузки, включая subqueryload и joinload с коллекциями.

Предупреждение

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

В частности, обычно невозможно использовать этот параметр с загруженными коллекциями (т.е. любой lazy = join или 'subquery), поскольку эти коллекции будут очищены для новой загрузки, если они встречаются в последующей партии результатов. В случае "загрузки подзапроса" получается полный результат для всех строк, который обычно побеждает цель yield_per().

Также обратите внимание, что хотя yield_per() установит параметр исполнения stream_results в значение True, в настоящее время это понимается только на диалекте psycopg2, который будет передавать результаты с использованием курсоров на стороне сервера вместо предварительного буфера всех строк для этого запроса. Другие DBAPI предварительно загружают все строки, прежде чем сделать их доступными. Использование необработанных строк базы данных в памяти намного меньше, чем использование объекта с отображением ORM, но его следует учитывать при бенчмаркинге.

У меня действительно есть проблема с пониманием того, как работает yield_per() и в чем проблема с использованием этого метода. Также верный способ обхода этих проблем и продолжать использовать эту функцию для итерации над огромным количеством строк.

Меня интересует вся необходимая конструктивная информация, но здесь есть некоторые подсказки:

  • Как может быть несколько экземпляров одной и той же строки? Только через отношения (если две строки итерационной таблицы имеют FK в той же строке в другой таблице)? Есть ли проблема, если вы не знаете, что это происходит, или вы только читаете атрибуты отношений?
  • lazy = join или 'subquery не возможны, но почему именно? Оба они являются частью вашего запроса, на который вы вызываете yield_per().
    • Если они очищаются в последующей партии результатов, просто загрузите ее снова. Так где же проблема? Или это единственная проблема, из-за которой вы теряете изменения в ваших отношениях, если они вносили изменения?
    • В случае загрузки подзапроса, почему все строки выбраны? SQL Server, возможно, придется сохранить большую таблицу, но почему бы просто не возвратить результат пакетами один за другим для всего запроса?
    • В примере в yield_per() doc q = sess.query(Object).yield_per(100).options(lazyload('*'), joinedload(Object.some_related)) они деактивируют eagerload с помощью lazyload('*'), но сохраняют единую объединенную нагрузку. Есть ли способ использовать yield_per() с eagerload? Каковы условия?
  • Говорят, что psycopg2 - это единственный DBAPI, который поддерживает поток потока. Итак, это единственный DBAPI, который вы можете использовать с yield_per()? Насколько я понимаю, yield_per использует функцию cursor.fetchmany() () функции DBAPI, которая поддерживает многие из них. И насколько я понимаю, cursor.fetchmany() поддерживает выборку только частей результата и не извлекает все (если он будет извлекать все, почему функция существует?)
  • У меня такое ощущение, что yield_per() полностью безопасен (даже с eagerload), если вы только читаете доступ (например, для статистики). Это правильно?
4b9b3361

Ответ 1

Обе проблемные стратегии загрузки вызывают исключения, если вы пытаетесь использовать их с yield_per, поэтому вам не нужно слишком беспокоиться.

Я считаю, что единственная проблема с subqueryload заключается в том, что пакетная загрузка второго запроса не реализована (пока). Ничто не изменится семантически, но если вы используете yield_per, у вас, вероятно, есть действительно хорошая причина не хотеть сразу загружать все результаты. Поэтому SQLAlchemy вежливо отказывается идти против ваших желаний.

joinedload немного более тонкий. Это запрещено только в случае коллекции, где первичная строка может иметь несколько связанных строк. Скажем, что ваш запрос производит такие исходные результаты, где A и B являются первичными ключами из разных таблиц:

 A | B 
---+---
 1 | 1 
 1 | 2 
 1 | 3 
 1 | 4 
 2 | 5 
 2 | 6 

Теперь вы получите их с помощью yield_per(3). Проблема заключается в том, что SQLAlchemy может ограничивать только то, сколько он извлекает по строкам, но он должен возвращать объекты. Здесь SQLAlchemy видит только первые три строки, поэтому создает объект A с ключом 1 и тремя B детьми: 1, 2 и 3.

Когда он загружает следующую партию, он хочет создать новый объект A с ключом 1... ах, но у него уже есть один из них, поэтому нет необходимости его снова создавать. Дополнительные B, 4, теряются. (Таким образом, нет, даже чтение объединенных коллекций с yield_per небезопасно - фрагменты ваших данных могут отсутствовать.)

Вы могли бы сказать "ну, просто продолжайте читать строки, пока не получите полный объект", но что, если у этого A есть сто детей? Или миллион? SQLAlchemy не может разумно гарантировать, что он может делать то, что вы просили, и давать правильные результаты, поэтому он отказывается попробовать.


Помните, что DBAPI разработан таким образом, что любая база данных может использоваться с одним и тем же API, даже если эта база данных не поддерживает все функции DBAPI. Подумайте, что DBAPI разработан вокруг курсоров, но MySQL на самом деле не имеет курсоров! Адаптеры DBAPI для MySQL должны подделать их.

Итак, пока cursor.fetchmany(100) будет работать, вы можете увидеть из исходный код MySQLdb, который он не получает лениво с сервера; он извлекает все в один большой список, затем возвращает срез, когда вы вызываете fetchmany.

То, что поддерживает psycopg2, - это истинная потоковая передача, где результаты запоминаются постоянно на сервере, и ваш процесс Python видит только несколько из них за раз.

Вы все еще можете использовать yield_per с MySQLdb или любым другим DBAPI; что весь смысл дизайна DBAPI. Вам придется оплачивать стоимость памяти для всех необработанных строк, скрытых в DBAPI (которые являются кортежами, довольно дешевыми), но вам также не придется оплачивать все объекты ORM одновременно.