память-эффективный встроенный итератор/генератор 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 генераторы?
ответ на этот вопрос, кажется, указывает на то, что потребление памяти не следует ожидать.
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()этот метод позволил мне сделать все виды агрегации данных без каких-либо опасные память.
NOTEthestream_resultsработает с Postgres иpyscopg2адаптер, но я думаю, что он не будет работать ни с DBAPI, ни с любым драйвером базы данных...есть интересный usecase в этом блоге это вдохновило меня выше метод.
Comments