[python] 메모리 효율적인 내장 SqlAlchemy 반복기 / 생성기?

SqlAlchemy를 사용하여 인터페이스하는 ~ 10M 레코드 MySQL 테이블이 있습니다. 이 테이블의 큰 하위 집합에 대한 쿼리는 데이터 세트의 한 입 크기의 청크를 지능적으로 가져 오는 내장 생성기를 사용한다고 생각했지만 너무 많은 메모리를 소비한다는 것을 발견했습니다.

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 내장 생성기와 관련하여 내가 놓친 것이 있습니까?

이 질문에 대한 대답 은 메모리 소비가 예상되지 않음을 나타내는 것 같습니다.



답변

대부분의 DBAPI 구현은 행을 가져올 때 완전히 버퍼링합니다. 따라서 일반적으로 SQLAlchemy ORM이 하나의 결과를 확보하기 전에 전체 결과 집합이 메모리에 있습니다.

그러나 Query작동 방식은 객체로 돌아 가기 전에 기본적으로 주어진 결과 세트를 완전히로드하는 것입니다. 여기서 근거는 단순한 SELECT 문 이상의 쿼리에 관한 것입니다. 예를 들어, 하나의 결과 집합에서 동일한 개체 ID를 여러 번 반환 할 수있는 다른 테이블에 대한 조인 (즉시로드와 공통)에서 전체 행 집합이 메모리에 있어야합니다. 그렇지 않으면 올바른 결과가 반환 될 수 있습니다. 부분적으로 만 채워질 수 있습니다.

따라서을 Query통해이 동작을 변경할 수있는 옵션을 제공합니다 yield_per(). 이 호출은 Query배치 크기를 제공하는 배치에서 행을 생성하도록합니다. 문서 상태에서 이것은 컬렉션을 열심히로드하지 않는 경우에만 적합하므로 기본적으로 수행중인 작업을 실제로 알고있는 경우입니다. 또한 기본 DBAPI가 행을 사전 버퍼링하는 경우에도 여전히 해당 메모리 오버 헤드가 있으므로 접근 방식은 사용하지 않는 것보다 약간만 더 잘 확장됩니다.

나는 거의 사용하지 않는다 yield_per(); 대신 창 함수를 사용하여 위에서 제안한 LIMIT 접근 방식의 더 나은 버전을 사용합니다. LIMIT 및 OFFSET에는 OFFSET 값이 매우 크면 쿼리가 느리고 느려진다는 큰 문제가 있습니다. N의 OFFSET로 인해 N 개의 행이 페이지를 넘기 게되므로 같은 쿼리를 한 번이 아니라 50 번 수행하는 것과 같습니다. 더 크고 많은 수의 행. 창 기능 접근 방식을 사용하여 선택하려는 테이블의 청크를 참조하는 “창”값 집합을 미리 가져옵니다. 그런 다음 각 창에서 한 번에 가져 오는 개별 SELECT 문을 내 보냅니다.

창 기능 접근 방식은 위키에 있으며 큰 성공을 거두었습니다.

또한 참고 : 모든 데이터베이스가 창 기능을 지원하는 것은 아닙니다. Postgresql, Oracle 또는 SQL Server가 필요합니다. 적어도 Postgresql을 사용하는 IMHO는 그만한 가치가 있습니다. 관계형 데이터베이스를 사용하는 경우 가장 좋은 방법을 사용하는 것이 좋습니다.


답변

저는 데이터베이스 전문가는 아니지만 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루프로 반복합니다.

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포스트 그레스와 함께 작동 pyscopg2어댑터,하지만 난 그것을하지 않습니다 어떤 DBAPI와 작업이나 데이터베이스 드라이버 추측 …

위의 방법에 영감을 준이 블로그 게시물 에는 흥미로운 사용 사례가 있습니다 .


답변

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


답변

Joel의 대답에 따라 다음을 사용합니다.

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


답변

이전에 모든 {OFFSET} 열을 찾아야하기 때문에 LIMIT / OFFSET을 사용하는 것은 좋지 않습니다. 따라서 더 큰 값은 OFFSET입니다. 나를 위해 창 쿼리를 사용하면 많은 양의 데이터가있는 큰 테이블에서도 나쁜 결과가 나타납니다 (처음 결과를 너무 오래 기다리므로 청크 된 웹 응답에 적합하지 않습니다).

여기에 제공된 최상의 접근 방식 https://stackoverflow.com/a/27169302/450103 . 제 경우에는 단순히 datetime 필드에 인덱스를 사용하고 datetime> = previous_datetime으로 다음 쿼리를 가져 오는 문제를 해결했습니다. 바보, 이전에 다른 경우에 해당 인덱스를 사용했지만 모든 데이터 창 쿼리를 가져 오는 것이 더 좋을 것이라고 생각했기 때문입니다. 제 경우에는 제가 틀 렸습니다.


답변

AFAIK, 첫 번째 변형은 여전히 ​​테이블에서 모든 튜플을 가져 오지만 (하나의 SQL 쿼리 사용) 반복 할 때 각 엔터티에 대한 ORM 프레젠테이션을 빌드합니다. 따라서 반복하기 전에 모든 항목의 목록을 작성하는 것보다 더 효율적이지만 모든 (원시) 데이터를 메모리로 가져와야합니다.

따라서 거대한 테이블에서 LIMIT를 사용하는 것이 좋은 생각처럼 들립니다.


답변