Zettelkasten

SSCursor는 결과를 서버에 두고 행 단위로 흘려보내 클라이언트 메모리 대신 커넥션 점유를 택한다

·수정 1

요약

  • buffered cursor(기본)는 execute() 시점에 전체 결과셋을 클라이언트 메모리로 한 번에 받고, SSCursor(server-side/unbuffered)는 결과를 서버에 둔 채 fetch마다 행을 흘려보낸다.
  • 트레이드오프는 "클라이언트 메모리·긴 단일 블로킹 ↔ 커넥션/서버 점유 시간"의 맞교환이다. 대용량 결과를 메모리에 올리지 못할 때 SSCursor를 쓴다.

본문

두 커서의 동작 차이

MySQLdb(mysqlclient)·PyMySQL의 기본 Cursor는 buffered(client-side)다. 내부적으로 mysql_store_result()를 호출해 execute() 순간 전체 결과셋을 서버→클라이언트로 전송받아 메모리에 보관한다. 이후 fetch*는 이미 받아둔 로컬 버퍼에서 꺼낼 뿐이다.

SSCursor/SSDictCursor는 server-side(unbuffered)다. mysql_use_result()를 써서 결과를 서버에 남겨두고, fetch 요청이 올 때마다 행을 전송한다. 클라이언트 메모리엔 항상 처리 중인 일부 행만 올라온다.

from MySQLdb.cursors import SSCursor

cur = conn.cursor()             # buffered: execute에서 전부 적재 → OOM 위험
cur.execute("SELECT * FROM huge_table")

cur = conn.cursor(SSCursor)     # server-side: 즉시 반환, 서버가 보관
cur.execute("SELECT * FROM huge_table")
for row in cur:                  # 한 행씩 받아옴 → 메모리 일정
    process(row)

스트리밍의 의미

"행을 한 줄씩 따로 TCP로 보낸다"는 뜻은 아니다. 서버가 결과를 들고 있다가 클라이언트가 읽어가는 만큼만 네트워크 버퍼에서 소비하는 구조다. 비유하면 buffered는 파일을 통째로 read(), SSCursor는 줄 단위로 흘려 읽는 제너레이터다.

핵심 트레이드오프 — CPU를 덜 쓰는 게 아니다

  • buffered: execute()가 전체 전송이 끝날 때까지 길게 블로킹 → 대신 끝나면 커넥션을 바로 놓아준다.
  • SSCursor: execute()는 즉시 반환, 각 fetch는 작은 단위라 긴 단일 블로킹이 없다 → 대신 끝까지 fetch할 때까지 커넥션을 점유한다.

총 CPU/네트워크 작업량은 SSCursor가 오히려 약간 더 많다(round trip이 많고 행당 오버헤드가 붙음). 이득은 양이 줄어서가 아니라 (1) 긴 블로킹이 짧은 fetch로 쪼개지고(chunked), (2) 클라이언트가 현재 행을 처리하는 동안 서버가 다음 행을 준비해 전송과 처리가 겹친다(pipelining). buffered는 "전송 전부 끝 → 그 다음 처리"로 직렬이다.

함정: 내 행 처리 로직이 느리면 커넥션을 점유한 채 서버가 결과셋/락/리소스를 그만큼 오래 들고 있게 된다(트랜잭션 안이면 그 트랜잭션이 길어짐). buffered는 빨리 다 빨아들이고 커넥션을 놓아주니 이 점에선 서버 친화적이다. 그래서 기본값이 buffered다.

드라이버별 차이 (주의 — mysqlclient ≠ PyMySQL)

mysqlclient PyMySQL
unbuffered rowcount -1(또는 부분값) uint64 sentinel(18446744073709551615) — 둘 다 "총 행 수를 미리 알 수 없다"가 핵심
결과 미소비 후 같은 커넥션에 다른 쿼리 Commands out of sync 에러 자동으로 남은 행을 drain하여 에러 없이 진행

공통점은 결과셋을 끝까지 소비(또는 close)해야 한다는 제약이고, 위반 시 동작만 드라이버마다 다르다.

SQLAlchemy에서 켜기

execution_options(stream_results=True) 또는 yield_per를 쓰면 server-side 스트리밍을 사용한다. MySQL dialect는 내부적으로 MySQLdb.cursors.SSCursor/pymysql.cursors.SSCursor를 쓴다.

관련 노트

참고