Python | DB・SQL:パフォーマンス

Python
スポンサーリンク

「パフォーマンス」って何?まずは感覚から

DB・SQL の「パフォーマンス」は、
ざっくり言うと「どれくらい速く・無駄なく・安定して動くか」です。

同じ結果を出すクエリでも、

1 秒で終わる書き方
1 分かかる書き方

が普通にあります。
しかも、見た目の SQL はちょっとしか違わなかったりします。

Python から DB を触るときのパフォーマンスは、

SQL の書き方
インデックスの有無
クエリの回数
トランザクションの切り方
Python 側のループや処理の仕方

このあたりが絡み合って決まります。

ここでは、初心者がまず押さえるべき「パフォーマンスのツボ」を、
例とコード付きでかみ砕いていきます。


一番効くのは「SQL の書き方」と「インデックス」

フルスキャンとインデックスの違いをイメージする

テーブルに 10 行しかないうちは、どんな SQL でもだいたい速いです。
問題は、10 万行、100 万行になってきたとき。

例えば、users テーブルがあるとします。

CREATE TABLE users (
    id    INTEGER PRIMARY KEY,
    name  TEXT NOT NULL,
    email TEXT NOT NULL
);
SQL

ここに 100 万件のユーザーが入っているとします。

「email で 1 人を探したい」ときのクエリはこうです。

SELECT id, name, email
FROM users
WHERE email = 'taro@example.com';
SQL

インデックスがないと、DB は「上から全部なめて探す」ことになります。
これをフルスキャン(全件走査)と言います。

インデックス(索引)を張ると、
「電話帳のように、email で素早く探せる」ようになります。

CREATE INDEX idx_users_email ON users(email);
SQL

これだけで、同じクエリが何十倍も速くなることがあります。

パフォーマンスで一番効くのは、
「どのカラムにインデックスを張るか」と「それを活かす WHERE や JOIN の書き方」です。

Python からのクエリでも中身は同じ

Python から書くとこうですが、

cur.execute(
    "SELECT id, name, email FROM users WHERE email = ?",
    ("taro@example.com",),
)
row = cur.fetchone()
Python

ここで速いか遅いかを決めているのは、
Python ではなく「裏で動いている SQL とインデックス」です。

だからこそ、

「Python の書き方をいじる前に、SQL とインデックスを見直す」

という順番が大事になります。


「クエリの回数」を減らすのも超重要

N+1 問題をイメージで理解する

よくあるやらかしが「N+1 問題」です。
例えば、ユーザーとその注文を表示したいとします。

ダメなパターンはこうです。

  1. ユーザー一覧を取るクエリ(1 回)
SELECT id, name FROM users;
SQL
  1. 各ユーザーごとに注文を取るクエリ(ユーザー数 N 回)
SELECT item, price FROM orders WHERE user_id = ?;
SQL

ユーザーが 1000 人いたら、
クエリは 1 + 1000 = 1001 回になります。

Python で書くと、こんな感じです。

cur.execute("SELECT id, name FROM users")
users = cur.fetchall()

for user in users:
    cur.execute(
        "SELECT item, price FROM orders WHERE user_id = ?",
        (user[0],),
    )
    orders = cur.fetchall()
    # ここで表示処理…
Python

これがまさに N+1 問題です。
クエリの回数がユーザー数に比例して増えていきます。

JOIN で 1 回にまとめる

同じことを、JOIN で 1 回のクエリにまとめるとこうなります。

SELECT
    users.id,
    users.name,
    orders.item,
    orders.price
FROM users
LEFT JOIN orders
    ON users.id = orders.user_id;
SQL

Python ではこう。

cur.execute(
    """
    SELECT
        users.id,
        users.name,
        orders.item,
        orders.price
    FROM users
    LEFT JOIN orders
        ON users.id = orders.user_id
    """
)
rows = cur.fetchall()
Python

クエリは 1 回だけです。
あとは Python 側で「ユーザーごとにグルーピング」すればよいだけ。

パフォーマンスを上げるときは、

「同じことを、より少ないクエリ回数でできないか?」

という視点がとても大事です。


Python 側の「無駄なループ」と「無駄な変換」を減らす

1 行ずつ SELECT して処理する vs まとめて取る

例えば、10000 行を処理したいときに、
こんな書き方をしてしまうことがあります。

for i in range(10000):
    cur.execute("SELECT ... WHERE id = ?", (i,))
    row = cur.fetchone()
    # 処理
Python

これは「10000 回クエリを投げている」ので、かなり遅くなります。

できるだけ、

「1 回のクエリで必要な行を全部取る」
「Python 側はその結果をループするだけ」

という形に寄せていきます。

cur.execute("SELECT id, name FROM users")
rows = cur.fetchall()

for row in rows:
    # 処理
Python

不要な Python 側の処理を SQL に寄せる

例えば、「ある条件で絞り込みたい」ときに、
全部取ってから Python でフィルタしてしまうことがあります。

cur.execute("SELECT id, name, age FROM users")
rows = cur.fetchall()

adults = [r for r in rows if r[2] >= 20]
Python

これは、「本当は DB が得意なことを Python にやらせている」状態です。

素直に SQL に任せた方が速くてシンプルです。

cur.execute(
    "SELECT id, name, age FROM users WHERE age >= ?",
    (20,),
)
adults = cur.fetchall()
Python

パフォーマンスを考えるときは、

「これは DB にやらせた方が速くないか?」

という問いを常に持っておくと、無駄な Python 側処理を減らせます。


トランザクションとバルク処理もパフォーマンスに直結する

1 件ずつ commit すると激遅になる

INSERT を 1 件ずつ commit するのは、典型的な遅くなるパターンです。

for user in users:
    cur.execute("INSERT INTO users (name, email) VALUES (?, ?)", user)
    conn.commit()  # 毎回 commit は重い
Python

commit は「トランザクションを確定する重い操作」なので、
これを何千回も繰り返すと、あっという間に遅くなります。

まとめて INSERT+まとめて commit

バルク処理のところでもやりましたが、
パフォーマンスを意識するなら、こう書き換えます。

cur.executemany(
    "INSERT INTO users (name, email) VALUES (?, ?)",
    users,
)
conn.commit()
Python

あるいは、バッチに分けるならこう。

batch_size = 1000

for batch in chunked(users, batch_size):
    cur.executemany(
        "INSERT INTO users (name, email) VALUES (?, ?)",
        batch,
    )
    conn.commit()
Python

「クエリの回数」と「commit の回数」を減らすことは、
パフォーマンス改善の超基本です。


「測る」ことをサボらない(体感ではなく数字で見る)

print でざっくり計測するだけでも価値がある

パフォーマンスは、感覚だけで判断するとだいたい外れます。
必ず「測る」ことが大事です。

Python なら、まずはこれで十分です。

import time

start = time.time()

# ここに処理を書く
cur.execute("SELECT ...")
rows = cur.fetchall()

end = time.time()
print("elapsed:", end - start, "sec")
Python

同じ処理を、

インデックスなし
インデックスあり

で比べてみると、「こんなに違うのか」と実感できます。

小さなデータでは差が見えないことも理解しておく

10 行、100 行くらいだと、
どんな書き方をしても「一瞬」で終わることが多いです。

パフォーマンスの差が見え始めるのは、
1 万行、10 万行、100 万行といった規模になってからです。

だからこそ、

「今は小さいけど、将来データが増えたときに詰まらない設計」

を意識しておくのが大事です。

インデックスを張る
N+1 を避ける
バルク処理を使う
無駄なクエリを減らす

こういった「基本の型」を、
小さいうちから身につけておくと、
後でスケールしたときに慌てずに済みます。


まとめ(パフォーマンスは「SQL・インデックス・クエリ回数」が土台)

Python × DB × SQL のパフォーマンスを、初心者目線で整理するとこうなります。

一番効くのは「SQL の書き方」と「インデックス」。WHERE や JOIN に使うカラムにはインデックスを張る。
クエリの回数を減らすことが超重要。N+1 を避けて、JOIN やバルク処理で「まとめて」取る・入れる。
DB が得意なこと(絞り込み・集計・ソート)はできるだけ SQL に任せて、Python 側で無駄なループやフィルタをしない。
トランザクションの commit を細かく切りすぎない。バッチ単位でまとめて commit する。
必ず「測る」。小さなデータでは差が見えないので、ある程度の件数で時間を計測して比較する。

タイトルとURLをコピーしました