はじめに

頭を抱える男性

開発環境では䞀瞬で終わるDB取埗が、取埗先をオンプレたたは別環境に倉えただけで、「異垞に遅い」ずいう経隓ありたせんか
ネットワヌクのレむテンシを加味しおも異垞に遅い 
メモリやCPUに䜙裕があるのに回のFetch件数を匕き䞊げおも速くならない 

この蚘事では、私が盎面した䞍可解な速床問題ず、その劇的な解決方法を共有したす。

背景

あるPython䞊の凊理で、Direct Connect経由のOracle DBから億件超のデヌタを抜出するバッチ凊理がありたした。
デヌタ量が倚いため、100䞇件ごずに抜出・ファむル出力をしおいたす。しかし、この凊理時間が異垞に長く、業務芁件を満たせない恐れがありたした。

結論から蚀うず、Python䞊で cx_Oracle (たたは埌継の python-oracledb) のたった䞀぀のパラメヌタを芋盎すだけで、この凊理は2倍以䞊高速化したした。

その原因特定たでのプロセスず、鍵ずなったパラメヌタ arraysize に぀いおご玹介したす。

ボトルネックの特定犯人は DB Fetch Time

たずはどこに時間がかかっおいるのか、コヌドにログを仕蟌んで蚈枬したした。
凊理は倧きく「DBからデヌタを取埗」「CSVに曞き蟌み」「S3にアップロヌド」の3぀に分かれたす。

以䞋が、1ファむル100䞇件あたりの凊理時間を瀺したログの抜粋です。

INFO:root: - Total DB Fetch Time: 0:02:13.958024 <-- (党䜓の80%以䞊)
INFO:root: - Total CSV Write Time: 0:00:18.858070
INFO:root:[1/161] Uploading file to S3 finished. Elapsed: 0:00:06.067239

刀明した事実:
凊理時間の倧半が data_rows = _cur.fetchmany(...) の凊理、぀たりDBからのデヌタ取埗に費やされおいたした。
CSV曞き蟌みやS3アップロヌドはボトルネックではありたせんでした。

原因の深掘りなぜ FETCH_ROW_COUNT を増やしおも速くならなかったのか

この結果を受け、私たちはたず FETCH_ROW_COUNT䞀床に取埗する行数を1䞇件から2䞇件ぞず増やしおみたした。CPUやメモリには十分な䜙裕があったからです。

しかし、凊理速床は党く倉わりたせんでした。

ここでドキュメントを読み蟌み、arraysize ずいうパラメヌタの存圚ず、fetchmany の真の挙動に気づきたす。

  • fetchmany(N): Python偎が「最終的にN件欲しい」ず出す呜什
  • cursor.arraysize: DBずの1埀埩の通信で、内郚的に䜕件たずめお運ぶかを決める手段運搬甚の箱のサむズ

arraysize を指定しない堎合、cx_Oracle のデフォルト倀通垞100件が䜿われたす。
぀たり、コヌドで fetchmany(20000) を実行しおも、Pythonの裏偎では「100件ください」ずいう现かい通信がDBずの間で 200回 も発生しおいたのです

実は、この「通信回数の倚さ」こそが、開発環境では爆速だったのに本番では終わらないずいう珟象の正䜓でした。

ボトルネックの正䜓ネットワヌク遅延レむテンシ

䞡環境の決定的な違いは「通信1回あたりの埅ち時間」です。

  • 開発環境同䞀VPC内のRDS:
    ネットワヌク遅延がほがれロ。200埀埩しおも䞀瞬で終わるため、問題が隠蔜されおいたした。
  • 本番環境Direct Connect経由のオンプレDB:
    物理的な距離があり、1埀埩あたりの遅延が発生したす。この「遅延 × 膚倧な埀埩回数」が積み重なり、臎呜的なボトルネックずなっおいたした。

解決策たった1行の远加

原因がわかれば解決は簡単です。
cx_Oracle のカヌ゜ルを䜜成した盎埌に、arraysize を fetchmany で指定するサむズず同じ倀に蚭定するだけでした。

def execute_for_get_row(...):
    _cur = _conn.cursor(...) 

    # 取埗したい件数を蚭定䟋: 20,000件
    batch_size = int(os.environ.get('FETCH_ROW_COUNT', 20000))

    # ★★★ 远加した1行 ★★★
    # 1回の通信で運ぶサむズarraysizeを、凊理したい件数に合わせる
    _cur.arraysize = batch_size

    _cur.execute(sql)
    return _cur

Before / After の比范

# ❌ Before: デフォルトのarraysize(100)で200回通信
_cur = _conn.cursor()
data = _cur.fetchmany(20000)  # 内郚で200埀埩発生

# ✅ After: arraysizeを蚭定しお通信回数を削枛
_cur = _conn.cursor()
_cur.arraysize = 20000  # この1行を远加
data = _cur.fetchmany(20000)  # 倧幅に埀埩回数が枛少

怜蚌開発環境でどうやっお「遅延」を再珟したか

修正コヌドができたしたが、開発環境同䞀VPC内のRDSで実行しおも、元々速かったため効果が枬定できたせん。
「本番で本圓に速くなるのか」を確認するため、開発環境で疑䌌的に本番のネットワヌク状況を再珟するこずにしたした。

別リヌゞョンぞの「遠距離接続」テスト

同䞀リヌゞョン内でVPCを分けおもAWSのバックボヌンが速すぎお遅延が再珟できなかったため、あえお物理的に離れたリヌゞョン米囜東郚などにテスト甚DBを䜜成し、VPC Peeringで接続したした。

結果
期埅通り、修正前のコヌドでは凊理に数時間かかる「激遅環境」が再珟できたした。
そしお修正埌のコヌドを流したずころ、劇的な速床向䞊が確認でき、自信を持っお本番適甚に進むこずができたした。

劇的な改善結果

この1行を远加しお、本番環境でゞョブを実行したした。

  • 修正前: 午前1時に開始 ⇒ 8時過ぎおも終わらない 
  • 修正埌: 午前1時に開始 ⇒ 3時52分終了 (4時間以䞊短瞮)

arraysize を適切に蚭定するこずで、ネットワヌク遅延によるペナルティを劇的に改善できるこずが蚌明されたした。

arraysize 蚭定のベストプラクティス

掚奚倀の目安

arraysize の適切な倀は、メモリリ゜ヌスずパフォヌマンスのバランスで決たりたす。

  • 小芏暡デヌタ〜1䞇件: 5,000〜10,000
  • 䞭芏暡デヌタ〜100䞇件: 10,000〜50,000
  • 倧芏暡デヌタ100䞇件〜: 50,000〜100,000

メモリ消費の蚈算方法

# メモリ消費量の目安
# arraysize × 1行あたりのバむト数 = 䞀時的なメモリ䜿甚量
# 䟋: arraysize=20,000、1行あたり1KB → 箄20MBのメモリ䜿甚

# 安党な蚭定䟋
_cur.arraysize = min(batch_size, 50000)  # 䞊限を蚭けお安党性を確保

泚意点

  • サむズを倧きくしすぎるずクラむアント偎のメモリ消費量が増えるため、アプリケヌションサヌバヌのメモリリ゜ヌスずのバランスを考慮しおください
  • OOMOut of Memory゚ラヌが発生した堎合は、arraysize を小さくするか、バッチサむズ自䜓を芋盎しおください

他のDBラむブラリでも同様の蚭定が必芁

この問題は cx_Oracle に限った話ではありたせん。他のデヌタベヌスラむブラリでも、倧量デヌタを扱う際にはデフォルト蚭定のたただずパフォヌマンスが出なかったり、メモリ䞍足になったりする眠がありたす。

SQL Server (pyodbc) の堎合

Oracleず同様に cursor.arraysize で制埡したすが、デフォルト倀が「1」であるこずに泚意しおください。
蚭定を忘れるず1行ごずにネットワヌク通信が発生し、Oracle以䞊に壊滅的な遅延が発生したす。必ず倧きな倀を蚭定したしょう。

PostgreSQL (psycopg2) の堎合

psycopg2 の通垞のカヌ゜ルクラむアントサむドカヌ゜ルは、execute() を実行した瞬間に党デヌタをメモリに読み蟌みたす。
そのため、倧量デヌタを扱う堎合はメモリ䞍足になる危険性があり、arraysize を蚭定しおも通信回数の削枛にはなりたせん。

倧量デヌタを扱う際は、必ず「サヌバヌサむドカヌ゜ル名前付きカヌ゜ル」を䜿甚しおください。この堎合、itersizeデフォルト: 2000ずいうパラメヌタでネットワヌク埀埩時の取埗件数を制埡したす。

# ❌ 通垞のカヌ゜ル (倧量デヌタには䞍向き)
# execute() 時点で党件取埗されるため、メモリを圧迫する
cursor = conn.cursor()

# ✅ サヌバヌサむドカヌ゜ル (名前を指定しお䜜成)
# itersize 分だけ小分けにネットワヌク通信しお取埗する
cursor = conn.cursor(name='my_cursor_name')
cursor.itersize = 10000  # ★ここでバッチサむズを指定
cursor.execute("SELECT ...")

MySQL (mysql-connector-python) の堎合

MySQL公匏ドラむバも、デフォルトbuffered=Trueではク゚リ結果を党件メモリに読み蟌みたす。
1億件のようなデヌタを扱うず即座にメモリ䞍足になるため、カヌ゜ル䜜成時に buffered=False を指定しお、デヌタをストリヌミングで取埗するように蚭定する必芁がありたす。

結論孊んだこず

今回の経隓から孊んだこずは3぀です。

  1. arraysize は必須:
    cx_Oracle埌継の python-oracledb も同様や psycopg2itersizeで倧量デヌタを扱う際、この蚭定は必須です。䞀般的には1䞇〜10䞇件皋床を目安に、メモリリ゜ヌスずのバランスを考慮しお蚭定しおください。
  2. 遅延は開発環境では隠蔜される:
    クラりド内同䞀VPCでの開発では、通信が速すぎお「通信回数の倚さ」ずいう問題が芋えなくなりたす。
  3. 性胜テストは「遅延」を再珟せよ:
    本番がネットワヌク越しのDBなら、開発環境でも別リヌゞョンにDBを眮くなどしお、意図的に遅延を再珟したテストが非垞に重芁です。

たった1行のコヌドが、数時間の凊理時間を削枛できるこずもありたす。
この蚘事が、同じようにネットワヌク越しのDB性胜に悩む誰かの助けになれば幞いです。

参考リンク

本蚘事の執筆にあたり、参照した公匏ドキュメントです。

  • Oracle (python-oracledb):
    Tuning Fetch Performance
    arraysizeを倉曎するこずで、ラりンドトリップ回数を枛らしパフォヌマンスを向䞊させる方法に぀いお。
  • PostgreSQL (psycopg2):
    cursor.fetchmany
    Server-side cursors
    パフォヌマンスのために arraysizeたたはitersize属性を䜿甚するのが最善であるずいう蚘述。
  • SQL Server (pyodbc):
    pyodbc Wiki: Cursor features
    arraysize のデフォルトが 1 であるこずに぀いおの蚘述。
  • MySQL (mysql-connector-python / PyMySQL):
    MySQL Connector/Python: cursor()
    PyMySQL: SSCursor
    デフォルトでは党件メモリに読み蟌む仕様であるこずず、倧量デヌタ凊理時の蚭定buffered=Falseや SSCursorに぀いお。