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


90

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

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


나는 그것이 "사물"을 산출한다는 점을 제외하고는 매우 유사한 것을 가지고 있습니다. 모든 다른 솔루션보다 더 나은 작품
iElectric

2
Thing.id> lastThingID 아닌가요? 그리고 "행"은 무엇입니까?
synergetic

답변:


118

대부분의 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는 그만한 가치가 있습니다. 관계형 데이터베이스를 사용하는 경우 가장 좋은 방법을 사용하는 것이 좋습니다.


쿼리가 ID를 비교하기 위해 모든 것을 인스턴스화한다고 언급했습니다. 기본 키를 기준으로 정렬하고 연속 된 결과 만 비교하여이를 피할 수 있습니까?
Tobu

문제는 ID가 X 인 인스턴스를 생성하면 애플리케이션이이를 보유한 다음이 엔티티를 기반으로 결정을 내리고 심지어 변형 할 수도 있다는 것입니다. 나중에 아마도 (실제로 일반적으로) 바로 다음 행에서도 동일한 ID가 결과에 다시 나타나서 컬렉션에 더 많은 콘텐츠를 추가 할 수 있습니다. 따라서 응용 프로그램은 불완전한 상태의 개체를 받았습니다. 정렬은 여기에서 도움이되지 않습니다. 가장 큰 문제는 즉시로드의 작동입니다. "joined"와 "subquery"로드 모두 다른 문제가 있습니다.
zzzeek

나는 "다음 행이 콜렉션을 업데이트한다"는 것을 이해했다.이 경우 콜렉션이 완료되는시기를 알기 위해 하나의 db 행만 살펴보면된다. 즉시로드의 구현은 정렬과 협력해야하므로 컬렉션 업데이트는 항상 인접한 행에서 수행됩니다.
Tobu

yield_per () 옵션은 생성하는 쿼리가 부분 결과 집합을 제공하는 것과 호환된다는 확신을 가질 때 항상 있습니다. 모든 경우에이 동작을 활성화하기 위해 며칠 동안 마라톤 세션을 보냈습니다. 즉, 프로그램이 그중 하나를 사용할 때까지 실패한 가장자리가 항상 모호했습니다. 특히 주문에 의존하는 것은 가정 할 수 없습니다. 항상 그렇듯이 실제 코드 기여를 환영합니다.
zzzeek

1
postgres를 사용하고 있기 때문에 반복 가능한 읽기 읽기 전용 트랜잭션을 사용하고 해당 트랜잭션에서 모든 창 쿼리를 실행할 수있는 것처럼 보입니다.
schatten 2014 년

24

저는 데이터베이스 전문가는 아니지만 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와 작업이나 데이터베이스 드라이버 추측 ...

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


1
postgres 또는 mysql (with pymysql)에서 작업하는 경우 이것은 허용되는 대답 IMHO입니다.
Yuki Inoue

1
내 목숨을 구하고 내 쿼리가 더 느리고 느리게 실행되는 것을 보았습니다. 위의 pyodbc (sql 서버에서 postgres로)에서 계측했으며 꿈처럼 실행 중입니다.
에드 베이커

이것은 나에게 최고의 접근 방식이었습니다. ORM을 사용하면서 SQL을 내 방언 (Postgres)으로 컴파일 한 다음 위에 표시된대로 연결 (세션이 아님)에서 직접 실행해야했습니다. 이 다른 질문에서 찾은 컴파일 "방법" stackoverflow.com/questions/4617291 . 속도 향상이 컸습니다. JOINS에서 SUBQUERIES로 변경하면 성능도 크게 향상되었습니다. 또한 sqlalchemy_mixins를 사용하는 것이 좋습니다. smart_query를 사용하면 가장 효율적인 쿼리를 작성하는 데 많은 도움이됩니다. github.com/absent1706/sqlalchemy-mixins
Gustavo Gonçalves

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()필요한지 잘 모르겠습니다 . 첫 번째 통화 이후 속도가 많이 향상되었습니다.
hamx0r 2015 년

@ hamx0r 나는 이것이 오래된 주석이라는 것을 알고 있으므로 후손을 위해 남겨 둡니다. .all()things 변수가 없으면 len ()을 지원하지 않는 쿼리가 있습니다.
David

9

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

things = query.slice (start, stop) .all ()은 끝에 []를 반환하고 while 루프는 중단되지 않습니다
Martin Reguly

4

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

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


3

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

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

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.