0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

SQLを書きながら「速いSQLってどう書くんだろう」と考え始めたとき、最初に腹落ちした気づきの共有です。

「タスクが全部完了したか」を確認する処理で、最初はこう考えました。

完了したレコードを全部数えて、件数が想定どおりなら「全部完了」とみなす

でも、完了を全部数えなくても「未完了が0件か」さえ見れば済む。
この小さな発想の転換で、SQLもコードもシンプルで速くなりました。

この記事で分かること

  • 「全部完了したか」を、完了件数ではなく未完了の不在で判定する考え方
  • NOT EXISTS が COUNT(*) より速くなりやすい理由
  • Python には true / false だけ返す書き方
  • そこから見える「速いSQL」の一般原則

前提: PostgreSQL + psycopg2 を例にします。
MySQL など他の行指向RDBでも接続部分を差し替えればそのまま使えます
(考え方自体はRedshiftのような列指向DBでも通用しますが、インデックス前提の話は当てはまらない点に注意)。

1. 困りごと:完了を全部数えるのは重い

「タスクが全部完了したら次へ進みたい」。よくある要件です。
最初に考えたのは「完了を全部数えて、件数が想定どおりなら完了とみなす」でした。

でもこれ、完了が増えるほど数える対象が増えます。
1万件完了していたら1万件を数える。「全部終わったか知りたいだけ」なのに重い。

2. 発想の転換:未完了を探すほうがラク

「全部完了した」を裏返すと、こうです。

未完了(とエラー)の行が1件も存在しない

完了を数えなくても、未完了が0件だと確認できれば「全部完了」と言える。
「全部できたか?」を「やり残しはないか?」に変えるだけです。

3. SQL:NOT EXISTS で判定する

「存在しないこと」を確認する構文が NOT EXISTS です。

-- 未完了(pending)・エラー(error)が1件もなければ true(=全部完了)
SELECT NOT EXISTS (
    SELECT 1
    FROM tasks
    WHERE status IN ('pending', 'error')
) AS all_completed;

該当が1件もなければ true、1件でもあれば false。これだけです。

なぜ COUNT(*) ではなく NOT EXISTS か

同じ判定は COUNT(*) でも書けます。

SELECT COUNT(*) = 0 AS all_completed
FROM tasks
WHERE status IN ('pending', 'error');

違いはやる仕事の量です。

書き方 挙動
COUNT(*) 該当行を最後まで全部数える
NOT EXISTS 該当行を1件見つけた時点で打ち切れる

EXISTS は「1件でもあるか?」を問うので、最初の1件が見つかった瞬間に探索をやめられます。
これが速さの正体です。

補足: 実際の最適化はオプティマイザとインデックス次第で、COUNT がうまく処理されることもあります。
ただ「あるか無いかだけ知りたい」意図が明確な NOT EXISTS のほうがDBの挙動と噛み合いやすい、と理解しておくと安全です。
status にインデックスがあるとさらに効きます。気になるときは EXPLAIN ANALYZE で実行計画を確認してください。

4. Python連携:判定はSQLに任せる

ポイントは未完了の行そのものをPythonに持ってこないこと。
判定はSQL側で済ませ、Pythonは true / false だけ受け取ります。
転送量が最小になり、ロジックもシンプルになります。

import psycopg2


def is_all_completed(conn) -> bool:
    """未完了・エラーが0件なら True(=全部完了)を返す。"""
    sql = """
        SELECT NOT EXISTS (
            SELECT 1 FROM tasks WHERE status IN ('pending', 'error')
        )
    """
    with conn.cursor() as cur:
        cur.execute(sql)
        (all_completed,) = cur.fetchone()   # (True,) または (False,)
    return all_completed

使う側:

if is_all_completed(conn):
    run_finalize()            # 0件 → 完了処理へ
else:
    logger.info("未完了あり")

追記:ポーリングして完了を待つ

import time

def wait_until_completed(conn, interval_sec=30, max_tries=20) -> bool:
    for _ in range(max_tries):
        if is_all_completed(conn):
            return True
        time.sleep(interval_sec)
    return False   # タイムアウト

毎回「未完了を1件探す」だけなので、何度呼んでも軽いのがポイントです。

5. 一般化:速いSQLとは「処理しなくて済むように書いたSQL」

NOT EXISTS が速かったのは、処理を頑張って速くしたからではなく、
そもそもやる仕事を減らしたからです。「全部数える」をやめて「1件探す」に変えただけ。

この視点で見ると、バラバラなテクニックが同じ原則の現れだと分かります。

テクニック 減らしているもの
NOT EXISTS / LIMIT 1 答えが出たら残りを処理しない
WHERE で早く絞り込む 対象の行数を減らす
インデックス 全行走査せず狙った行だけ読む
必要な列だけ SELECT(SELECT * を避ける) 読むデータ量を減らす

どれも「速く処理する」ではなく「処理する量・読む量を減らす」方向を向いています。

速いSQLとは、速く処理するSQLではなく、処理しなくて済むように書いたSQLである。

まとめ

  • 「全部完了したか」は、未完了の不在(NOT EXISTS)で判定できる
  • NOT EXISTS は1件見つけたら打ち切れるため、全件数える COUNT より速くなりやすい
  • Pythonには判定結果だけ返し、行そのものは転送しない
  • 速さの本質は「速く処理する」ではなく「処理する量を減らす」こと

参考:同じ原則で読める記事

「処理する量を減らす」視点で面白かった記事です。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?