ефективний пам’ять вбудований ітератор / генератор SqlAlchemy?


90

У мене є ~ 10M таблиці записів MySQL, з якою я взаємодію за допомогою 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?

Відповідь на це питання, мабуть, вказує на те, що споживання пам'яті очікувати не варто.


У мене є щось дуже схоже, за винятком того, що воно дає "річ". Працює краще за всі інші рішення
iElectric

2
Хіба це не Thing.id> lastThingID? А що таке "ряди"?
синергетика

Відповіді:


118

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

Але тоді, як Queryпрацює, це те, що він повністю завантажує заданий результат за замовчуванням, перш ніж повертати вам ваші об’єкти. Обґрунтування тут стосується запитів, які є більше, ніж простими операторами SELECT. Наприклад, при об'єднаннях до інших таблиць, які можуть повертати одну і ту ж ідентичність об'єкта кілька разів в одному наборі результатів (загальне для нетерплячого завантаження), повний набір рядків повинен бути в пам'яті, щоб правильні результати могли бути повернуті в іншому випадку колекції і такі може бути заселено лише частково.

Тож Queryпропонує можливість змінити цю поведінку за допомогою yield_per(). Цей виклик призведе Queryдо отримання рядків рядками, де ви надаєте йому розмір партії. Як зазначається у документах, це доречно лише у тому випадку, якщо ви не робите будь-якого нетерплячого завантаження колекцій, тому в основному, якщо ви дійсно знаєте, що робите. Крім того, якщо базовий DBAPI попередньо буферизує рядки, ця пам'ять все одно залишатиметься, тому підхід лише масштабується трохи краще, ніж не використовувати його.

Я майже ніколи не використовую yield_per(); натомість я використовую кращу версію підходу LIMIT, який ви запропонували вище, використовуючи функції вікна. LIMIT та OFFSET мають величезну проблему, оскільки дуже великі значення OFFSET спричиняють повільніший та повільніший запит, оскільки OFFSET з N змушує його переходити через N рядків - це все одно, що робити один і той же запит п'ятдесят разів замість одного, кожного разу читаючи більша і більша кількість рядів. За допомогою підходу з функцією вікна я попередньо отримую набір значень "вікно", які посилаються на фрагменти таблиці, яку я хочу вибрати. Потім я видаю окремі оператори SELECT, які кожен витягує з одного з цих вікон одночасно.

Підхід до функцій вікна знаходиться у вікі, і я користуюся ним із великим успіхом.

Також зверніть увагу: не всі бази даних підтримують функції вікна; вам потрібен Postgresql, Oracle або SQL Server. IMHO із використанням принаймні Postgresql однозначно варто - якщо ви використовуєте реляційну базу даних, ви можете скористатися найкращим.


Ви згадуєте, що Query інстанцирує все, щоб порівняти ідентичності. Чи можна цього уникнути шляхом сортування за первинним ключем та лише порівняння послідовних результатів?
Тобу

проблема полягає в тому, що якщо ви видаєте екземпляр з ідентифікатором X, програма отримує його, а потім приймає рішення на основі цієї сутності, а може навіть і мутує. Пізніше, можливо (насправді, як правило) навіть у наступному рядку, ця ж ідентичність повертається в результат, можливо, щоб додати більше вмісту до своїх колекцій. Тому заявка отримала об’єкт у неповному стані. сортування тут не допомагає, оскільки найбільша проблема полягає в роботі енергійного завантаження - як приєднане, так і завантаження підзапитів мають різні проблеми.
zzzeek

Я зрозумів, що "наступний рядок оновлює колекції", і в цьому випадку вам потрібно заглянути лише на один рядок db, щоб знати, коли колекції будуть завершені. Реалізація енергійного завантаження повинна була б співпрацювати з сортуванням, так що оновлення колекції завжди робиться в сусідніх рядках.
Тобу

Опція yield_per () завжди є, коли ви впевнені, що запит, який ви видаєте, сумісний з доставкою часткових наборів результатів. Я провів марафонський кількаденний сеанс, намагаючись увімкнути цю поведінку у всіх випадках, завжди були незрозумілі, тобто поки ваша програма не використовує один з них, ребра, які не вдалися. Зокрема, покладатися на замовлення не можна припустити. Як завжди, я вітаю фактичні внески коду.
zzzeek

1
Оскільки я використовую postgres, схоже, можна використовувати транзакцію "Повторюване читання" лише для читання та запустити всі віконні запити в цій транзакції.
schatten

24

Я не фахівець з баз даних, але коли використовую SQLAlchemy як простий рівень абстракції Python (тобто, не використовуючи об'єкт ORM Query), я придумав задовільне рішення для запиту таблиці в 300 мільйонів без вибуху пам'яті ...

Ось фіктивний приклад:

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)

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

while 'batch not empty':  # equivalent of 'while True', but clearer
    batch = proxy.fetchmany(100000)  # 100,000 rows at a time

    if not batch:
        break

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

proxy.close()

Цей метод дозволив мені робити всі види агрегування даних без будь-яких небезпечних витрат пам'яті.

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

У цій публікації в блозі є цікавий варіант використання, який надихнув мій вищезазначений метод.


1
Якщо хтось працює над postgres або mysql (with pymysql), це має бути прийнятою відповіддю IMHO.
Юкі Іноуе

1
Врятував моє життя, бачив, як мої запити працюють все повільніше. Я описав вище на pyodbc (від сервера sql до postgres), і він працює як мрія.
Ед Бейкер

Це був для мене найкращий підхід. Оскільки я використовую ORM, мені потрібно було скомпілювати SQL на свій діалект (Postgres), а потім виконати безпосередньо з підключення (а не з сеансу), як показано вище. Компіляцію "як зробити" я знайшов у цьому іншому запитанні stackoverflow.com/questions/4617291 . Поліпшення швидкості було великим. Перехід від JOINS до SUBQUERIES також був значним збільшенням продуктивності. Також рекомендуємо використовувати sqlalchemy_mixins, використання smart_query дуже допомогло побудувати найбільш ефективний запит. github.com/absent1706/sqlalchemy-mixins
Густаво Гонсалвес

14

Я вивчав ефективний обхід / підкачку за допомогою 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

Це здається дуже простим і швидким. Я не впевнений, що .all()це необхідно. Я помічаю, що швидкість значно покращилася після 1-го дзвінка.
hamx0r

@ hamx0r Я розумію, що це старий коментар, тому просто залиште його для нащадків. Без .all()змінної речей є запит, який не підтримує len ()
Девід

9

У дусі відповіді Джоеля я використовую наступне:

WINDOW_SIZE = 1000
def qgen(query):
    start = 0
    while True:
        stop = start + WINDOW_SIZE
        things = query.slice(start, stop).all()
        if len(things) == 0:
            break
        for thing in things:
            yield thing
        start += WINDOW_SIZE

things = query.slice (start, stop) .all () повернеться [] в кінці і while цикл ніколи не
перерветься

4

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

Найкращий підхід, наведений тут https://stackoverflow.com/a/27169302/450103 . У моєму випадку я вирішив проблему, просто використовуючи індекс у полі datetime та отримуючи наступний запит із datetime> = previous_datetime. Дурний, тому що я раніше використовував цей індекс у різних випадках, але думав, що для отримання всіх віконних запитів даних було б краще. У моєму випадку я помилився.


3

AFAIK, перший варіант все ще отримує всі кортежі з таблиці (з одним запитом SQL), але створює презентацію ORM для кожної сутності під час ітерації. Отже, це ефективніше, ніж створення списку всіх сутностей перед ітерацією, але вам все одно доведеться отримати всі (необроблені) дані в пам’ять.

Таким чином, використання LIMIT на величезних столах для мене здається гарною ідеєю.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.