эффективный в памяти встроенный итератор / генератор SqlAlchemy? - PullRequest
67 голосов
/ 12 сентября 2011

У меня есть таблица MySQL ~ 10M записей, с которой я взаимодействую с помощью SqlAlchemy. Я обнаружил, что запросы к большим подмножествам этой таблицы будут занимать слишком много памяти, хотя я думал, что использую встроенный генератор, который интеллектуально выбирает куски набора данных размером с укус:

for thing in session.query(Things):
    analyze(thing)

Чтобы избежать этого, я обнаружил, что должен создать свой собственный итератор, который кусается кусками:

lastThingID = None
while True:
    things = query.filter(Thing.id < lastThingID).limit(querySize).all()
    if not rows or len(rows) == 0: 
        break
    for thing in things:
        lastThingID = row.id
        analyze(thing)

Это нормально или мне чего-то не хватает в встроенных генераторах SA?

Ответ на на этот вопрос , кажется, указывает на то, что потребление памяти не ожидается.

Ответы [ 6 ]

105 голосов
/ 12 сентября 2011

Большинство реализаций DBAPI полностью буферизуют строки по мере их выборки - поэтому обычно, прежде чем SQLAlchemy ORM даже получит один результат, весь набор результатов находится в памяти.

Но тогда способ работы Queryявляется то, что он полностью загружает данный набор результатов по умолчанию, прежде чем вернуть вам ваши объекты.Обоснование здесь относится к запросам, которые относятся не только к простым инструкциям SELECT, но и к другим таблицам, которые могут возвращать одну и ту же идентичность объекта несколько раз в одном наборе результатов (что обычно при активной загрузке), полный набор строк должен находиться в памяти, поэтомучто правильные результаты могут быть возвращены - в противном случае коллекции и тому подобное могут быть заполнены только частично.

Таким образом, Query предлагает возможность изменить это поведение, то есть вызов yield_per () http://www.sqlalchemy.org/docs/orm/query.html?highlight=yield_per#sqlalchemy.orm.query.Query.yield_per.Этот вызов приведет к тому, что запрос выдаст строки в пакетах, где вы задаете размер пакета.Как указано в документации, это уместно только в том случае, если вы не выполняете какую-либо активную загрузку коллекций - так что это в основном, если вы действительно знаете, что делаете.А также, если базовые строки DBAPI предварительно буферизуют строки, это все равно приведет к дополнительным затратам памяти, поэтому подход масштабируется только немного лучше, чем без него.

Я почти никогда не использую yield_per () - вместо этого я используюлучшую версию подхода LIMIT, которую вы предлагаете выше, используя оконные функции.У LIMIT и OFFSET огромная проблема: очень большие значения OFFSET приводят к тому, что запрос становится все медленнее и медленнее, так как OFFSET из N заставляет его перелистывать N строк - это все равно, что выполнять один и тот же запрос пятьдесят раз вместо одного, каждый раз читаявсе большее и большее количество рядов.При подходе оконной функции я предварительно выбираю набор значений «окна», которые относятся к фрагментам таблицы, которую я хочу выбрать.Затем я генерирую отдельные операторы SELECT, которые каждый раз извлекают из одного из этих окон.

Подход к оконной функции есть в вики на http://www.sqlalchemy.org/trac/wiki/UsageRecipes/WindowedRangeQuery, и я использую его с большим успехом.

Также обратите внимание, что не все базы данных поддерживают оконные функции - вам нужны PG, Oracle или SQL Server.ИМХО использование хотя бы Postgresql определенно того стоит - если вы используете реляционную базу данных, вы можете использовать и лучшее.

12 голосов
/ 06 октября 2014

Я искал эффективный обход / разбиение по страницам с помощью SQLAlchemy и хотел бы обновить этот ответ.

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

Пример:

window_size = 10  # or whatever limit you like
window_idx = 0
while True:
    start,stop = window_size*window_idx, window_size*(window_idx+1)
    things = query.slice(start, stop).all()
    if things is None:
        break
    for thing in things:
        analyze(thing)
    if len(things) < window_size:
        break
    window_idx += 1
7 голосов
/ 03 мая 2018

Я не эксперт по базам данных, но при использовании SQLAlchemy в качестве простого уровня абстракции Python (т. Е. Без использования объекта запроса ORM) я нашел удовлетворительное решение для запроса таблицы из 300M строк без взрыва использования памяти...

Вот фиктивный пример:

from sqlalchemy import create_engine, select

conn = create_engine("DB URL...").connect()
q = select([huge_table])

proxy = conn.execution_options(stream_results=True).execute(q)

Затем я использую метод SQLAlchemy fetchmany(), чтобы перебрать результаты в цикле while:

empty = False
while not empty:
    batch = proxy.fetchmany(100000)  # 100,000 rows at a time

    if not batch:
        empty = True

    for row in batch:
        # Do your stuff here...

proxy.close()

Этот метод позволил мне выполнять все виды агрегации данных без каких-либо опасных накладных расходов памяти.

NOTE stream_results работает с Postgres и адаптером pyscopg2, но я думаю, он не будет работать ни с DBAPI, ни с любым драйвером базы данных ...

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

5 голосов
/ 29 апреля 2015

В духе ответа Джоэля я использую следующее:

WINDOW_SIZE = 1000
def qgen(query):
    start = 0
    while True:
        stop = start + WINDOW_SIZE
        things = query.slice(start, stop).all()
        if things is None:
            break
        for thing in things:
            yield(thing)
        start += WINDOW_SIZE
2 голосов
/ 17 февраля 2016

Использование LIMIT / OFFSET плохо, потому что вам нужно найти все столбцы {OFFSET} раньше, поэтому чем больше значение OFFSET, тем дольше вы получаете запрос.Использование оконного запроса для меня также дает плохие результаты для большой таблицы с большим объемом данных (первые результаты слишком долго ждут, что в моем случае это не очень хорошо для фрагментированного веб-ответа).

Лучший подход приведен здесь https://stackoverflow.com/a/27169302/450103. В моем случае я решил проблему, просто используя индекс в поле даты и времени и получая следующий запрос с датой времени> = предыдущая_дата времени.Глупо, потому что раньше я использовал этот индекс в разных случаях, но думал, что для извлечения всех данных оконный запрос будет лучше.В моем случае я ошибся.

2 голосов
/ 12 сентября 2011

AFAIK, первый вариант по-прежнему получает все кортежи из таблицы (с одним запросом SQL), но создает представление ORM для каждой сущности при итерации.Таким образом, это более эффективно, чем создание списка всех сущностей перед итерацией, но вам все равно нужно извлечь все (необработанные) данные в память.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...