память-эффективный встроенный итератор/генератор SqlAlchemy?



у меня есть таблица 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 генераторы?



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

663   6  

6 ответов:

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

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

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

Я почти никогда не использую yield_per() - вместо этого я использую лучшую версию предельного подхода, который вы предлагаете выше, используя оконные функции. Limit и Offset есть огромная проблема, что очень большие значения смещения заставляют запрос становиться все медленнее и медленнее, так как смещение N заставляет его пролистывать N строк - это похоже на выполнение одного и того же запроса пятьдесят раз вместо одного, каждый раз читая все большее и большее количество строк. С помощью метода оконной функции я предварительно извлекаю набор значений "окна", которые относятся к фрагментам таблицы, которую я хочу выбрать. Затем я создаю отдельные инструкции SELECT, которые каждый из них извлекает из одного из этих окон за раз.

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

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

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

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

пример:

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

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

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

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

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

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

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

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

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

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 batch == []:
        empty = True

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

proxy.close()

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

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

есть интересный usecase в этом блоге это вдохновило меня выше метод.

Comments

    Ничего не найдено.