データベース (= データの倉庫) に話しかける専用言語 のことです。
このプロジェクト (cherimi LINE CRM) で使っている Cloudflare D1 というサービスは、小さな SQLite データベースが Cloudflare のクラウドで動いている状態。そこに以下のような情報が棚に並んで保管されています:
users テーブル)tags, user_tags)messages)message_recipients)message_clicks)このデータに対して「取り出して」「追加して」「書き換えて」「消して」といった依頼をするための言葉が SQL です。
よくある依頼書の例:
SELECT display_name FROM users WHERE id = 6;
これを日本語に訳すと:
「ユーザー棚 (users) から、id が 6 番の人の display_name (表示名) を取ってきて」
倉庫番 (= D1) はこれを読んで棚を探しに行き、木村敏之 という答えを返してきます。
| SQL の動詞 | 意味 | 使う場面 |
|---|---|---|
SELECT | 取ってくる | 「VIP タグの人一覧を見たい」 |
INSERT | 新しく入れる | 「新しい友だちを user 棚に追加」 |
UPDATE | 書き換える | 「〇〇さんのブロック状態を解除済みに更新」 |
DELETE | 消す | 「テスト用の偽ユーザーを削除」 |
実際のコードでは、クエリに具体的な値を直接書かず、穴埋め形式で書きます。
SELECT display_name FROM users WHERE id = ?;
この ? に後から 6 という値を埋め込む。これを 「バインド (bind)」 と言います。
「複数のIDの人をまとめて取りたい」場合はこう書きます:
SELECT id, line_user_id FROM users WHERE id IN (?, ?, ?);
3つの ? に、たとえば 6, 7, 8 をバインドすると、「id が 6 か 7 か 8 のどれかの人を取ってきて」という依頼になります。
実際のコード (worker/src/handlers/api/messages.ts):
const placeholders = targetUserIds.map(() => '?').join(',');
const lineUsers = await env.DB
.prepare(
`SELECT id, line_user_id FROM users WHERE id IN (${placeholders})`
)
.bind(...targetUserIds)
.all();
これは配信対象の ID が N 個あれば、? を N 個並べるという意味です。
SELECT id, line_user_id FROM users WHERE id IN (?, ?, ?); ↓ バインド SELECT id, line_user_id FROM users WHERE id IN (6, 1, 2);
問題なし ? が 3 個なので D1 の倉庫番は余裕で処理できる。実際、QA テストでは 1人〜3人での検証だったので、ここはずっと通っていた。
SELECT id, line_user_id FROM users WHERE id IN (?, ?, ?, ?, ?, ...もっと続く... ?, ?, ?); ↓ ? が 20,000 個並ぶ
破綻 D1 (SQLite ベース) の倉庫番は 「一度に受け取れる ? の数は約 100 個まで」 という制限を持っています。これを超えると too many SQL variables 的なエラーで突き返されます。
? の個数は最大 3 個。制限に引っかからない。本番で 20,000 人に一斉送信しようとした瞬間に初めて破綻する、という潜在バグ。
SQL の問題だけでなく、20k 配信にはもう 2 つの落とし穴があります:
1人に送るのに 20 ミリ秒の間隔を空けて送信すると:
20,000 人 × 20ms = 400,000ms = 400 秒 = 約 6分40秒
Cloudflare Workers には「1回の処理は◯秒以内に終わらせる」という上限があり、長時間かかる処理は途中で強制終了される可能性がある。
仮に 7,500 人まで送った所で Worker が強制終了したとする。データベースには「7,500 人分は送った」という情報が部分的に残る。
これは SQL の問題ではなく「長時間処理の再開設計」の問題。配信履歴を「送信済み/送信中/失敗」でちゃんと記録し、どこから再開できるようにする必要がある。
20,000 人分の ID を一度に WHERE id IN (?,?,?,...) で渡すのではなく、80 人ずつに区切って 250 回投げる:
// 修正前 (1回で全部) WHERE id IN (?,?,?,...20,000個...) // 修正後 (80個ずつ 250 回) WHERE id IN (?,?,?,...80個...) ← 1回目 WHERE id IN (?,?,?,...80個...) ← 2回目 WHERE id IN (?,?,?,...80個...) ← 3回目 ... WHERE id IN (?,?,?,...80個...) ← 250回目
それぞれ倉庫番の制限内に収まるので確実に通る。結果を合算して返すようコードを書き換えればOK。
SQL は倉庫番への依頼書の書式。
クエリ は具体的に書いた 1 枚の依頼書。
? は依頼書の穴埋め欄で、そこに値を入れる (=バインドする)。
問題は「穴埋め欄を 20,000 個並べた依頼書は倉庫番が受け取れない」ということ。
修正は「依頼書を 80 個ずつに分けて 250 回投げる」。