SQLでランダムにデータを取得する場面は意外と多くあります。抽選システム・クイズのランダム出題・A/Bテストのサンプリング・テストデータの生成など、業務でもよく登場する要件です。
本記事では、MySQL・PostgreSQL・Oracle・SQL Serverそれぞれの構文から、大規模テーブルでも遅くならない高速化テクニック、シード付き再現可能ランダム、TABLESAMPLEによる統計サンプリングまで網羅します。
- DBMS別のランダムソート構文(ORDER BY RAND / RANDOM / NEWID / DBMS_RANDOM)
- ORDER BY RAND()がなぜ遅いのか、内部動作の仕組み
- 100万件超のテーブルでも高速なランダム取得パターン
- TABLESAMPLEによる統計サンプリング(PostgreSQL / SQL Server / Oracle)
- シード付きランダムで再現性のあるランダム取得
- 抽選・クイズ・A/Bテスト・データ品質チェックの実践パターン
DBMS別の基本構文
ランダムソートの書き方はDBMSによって異なります。まず各DBMSの構文をまとめます。
| DBMS | ランダム関数 | 基本構文 | 戻り値の範囲 |
|---|---|---|---|
| MySQL / MariaDB | RAND() |
ORDER BY RAND() |
0以上1未満の浮動小数 |
| PostgreSQL | RANDOM() |
ORDER BY RANDOM() |
0以上1未満の浮動小数 |
| Oracle | DBMS_RANDOM.VALUE |
ORDER BY DBMS_RANDOM.VALUE |
0以上1未満の浮動小数 |
| SQL Server | NEWID() |
ORDER BY NEWID() |
ランダムなGUID |
-- 全件をランダム順に取得 SELECT * FROM products ORDER BY RAND(); -- ランダムに5件取得 SELECT * FROM products ORDER BY RAND() LIMIT 5;
-- 全件をランダム順に取得 SELECT * FROM products ORDER BY RANDOM(); -- ランダムに5件取得 SELECT * FROM products ORDER BY RANDOM() LIMIT 5;
-- ランダムに5件取得
SELECT * FROM products
ORDER BY DBMS_RANDOM.VALUE
FETCH FIRST 5 ROWS ONLY;
-- ROWNUMを使う古い書き方
SELECT * FROM (
SELECT * FROM products
ORDER BY DBMS_RANDOM.VALUE
)
WHERE ROWNUM <= 5;
-- ランダムに5件取得 SELECT TOP 5 * FROM products ORDER BY NEWID();
SQL ServerのNEWID()は数値乱数ではなくGUID(グローバル一意識別子)を生成し、そのバイト列でソートします。統計的な均一性は問題ありませんが、シード指定はできません。
ORDER BY RAND()の仕組みとパフォーマンス問題
ORDER BY RAND()はシンプルで直感的ですが、テーブルが大きくなると極端に遅くなるという致命的な問題があります。まずその理由を理解しましょう。
内部処理の流れ
- 対象テーブルの全行を読み込む(フルテーブルスキャン)
- 各行に乱数値を付与する
- 乱数値で全行をソートする(O(N log N))
- 先頭のN件を返す
1万件 → 数十ms、10万件 → 数百ms、100万件 → 数秒〜数十秒。インデックスは一切使われず、毎回フルスキャンが走ります。データが少ない場合は問題ありませんが、本番環境の大規模テーブルには使わないようにしましょう。
EXPLAIN で確認する
EXPLAIN SELECT * FROM products ORDER BY RAND() LIMIT 5; -- type: ALL(フルスキャン) -- Extra: Using temporary; Using filesort -- ← 一時テーブル作成 + 全行ソートが走る
大規模テーブルでの高速化テクニック
テーブルが大きい場合はORDER BY RAND()を避け、以下のパターンを使います。
パターン1:IDにオフセットを使う方式(MySQL)
IDが連番の場合に有効な高速パターンです。RAND() * MAX(id)でランダムなIDを生成し、それ以上のレコードを取得します。
-- ランダムな1件を高速取得
SELECT * FROM products
WHERE id >= FLOOR(1 + RAND() * (SELECT MAX(id) FROM products))
LIMIT 1;
-- ランダムな5件を取得(重複の可能性あり)
SELECT p.*
FROM products p
JOIN (
SELECT FLOOR(1 + RAND() * (SELECT MAX(id) FROM products)) AS rand_id
FROM (SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3
UNION ALL SELECT 4 UNION ALL SELECT 5) AS n
) AS r ON p.id >= r.rand_id
GROUP BY r.rand_id
HAVING COUNT(*) > 0;
IDが連番でない場合(削除で欠番が発生している場合)、特定のIDに偏りが生じることがあります。厳密な均一性が必要な場合はサブクエリでROW_NUMBERを振り直すか、後述のTABLESAMPLE方式を検討してください。
パターン2:ROW_NUMBER()でランダムIDを指定する方式
IDに欠番がある場合でも均一なランダム取得が可能です。ウィンドウ関数のROW_NUMBER()を使い、連番を振り直してからランダムに選択します。
WITH numbered AS (
SELECT *,
ROW_NUMBER() OVER (ORDER BY id) AS rn,
COUNT(*) OVER () AS total
FROM products
),
random_picks AS (
SELECT FLOOR(1 + RAND() * total) AS pick
FROM (SELECT DISTINCT total FROM numbered) t
-- 5件分の乱数を生成
CROSS JOIN (SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3
UNION ALL SELECT 4 UNION ALL SELECT 5) n
)
SELECT n.*
FROM numbered n
JOIN random_picks rp ON n.rn = rp.pick;
パターン3:TABLESAMPLEによるサンプリング(PostgreSQL / SQL Server / Oracle)
TABLESAMPLEは行単位ではなくページ(ブロック)単位でサンプリングするため、フルスキャンを回避でき高速です。
TABLESAMPLEはMySQL(MariaDB含む)では使えません。MySQLで大規模テーブルのランダム取得を高速化する場合は、前述のID乱数方式またはROW_NUMBER()方式を使ってください。
-- SYSTEM: ブロック単位サンプリング(高速・非均一) -- 全行の約10%をサンプリング SELECT * FROM products TABLESAMPLE SYSTEM (10); -- BERNOULLI: 行単位サンプリング(均一・やや低速) SELECT * FROM products TABLESAMPLE BERNOULLI (5) ORDER BY RANDOM() LIMIT 10; -- REPEATABLE(seed) で再現可能なサンプリング SELECT * FROM products TABLESAMPLE SYSTEM (10) REPEATABLE (42);
-- 全行の約10%をサンプリング SELECT * FROM products TABLESAMPLE (10 PERCENT); -- 行数で指定する方法 SELECT * FROM products TABLESAMPLE (100 ROWS); -- REPEATABLE(seed) で再現可能 SELECT * FROM products TABLESAMPLE (10 PERCENT) REPEATABLE (12345);
-- BLOCK SAMPLE: ブロック単位(高速) SELECT * FROM products SAMPLE BLOCK (10); -- ROW SAMPLE: 行単位(均一) SELECT * FROM products SAMPLE (5); -- シード付きサンプリング SELECT * FROM products SAMPLE (10) SEED (42);
| 方式 | DBMS | 均一性 | 速度 | 再現性 |
|---|---|---|---|---|
| SYSTEM | PostgreSQL | △(ブロック依存) | ◎ | ○(REPEATABLE) |
| BERNOULLI | PostgreSQL | ◎ | ○ | ○(REPEATABLE) |
| TABLESAMPLE PERCENT | SQL Server | △(ブロック依存) | ◎ | ○(REPEATABLE) |
| SAMPLE BLOCK | Oracle | △(ブロック依存) | ◎ | ○(SEED) |
| SAMPLE(行) | Oracle | ◎ | ○ | ○(SEED) |
| ORDER BY RAND() | 全DBMS | ◎ | ×(N増で激遅) | ○(シード付き) |
TABLESAMPLEはデータ分析・統計サンプリングに向いています。ただし指定した割合は「目安」であり、毎回ぴったり同じ件数が返ってくるわけではありません。「ランダムにN件ちょうど取得したい」という要件にはORDER BY RAND() LIMIT Nか高速化パターンを使いましょう。
シード付きランダム(再現可能なランダム取得)
同じシード値を与えると毎回同じランダム順序が再現されます。テストやデバッグで「再現性のあるランダム」が必要な場合に使います。
-- シード42を固定すると毎回同じ順序 SELECT * FROM products ORDER BY RAND(42) LIMIT 10; -- 日付をシードにすることで「今日だけ固定」にする SELECT * FROM products ORDER BY RAND(UNIX_TIMESTAMP(CURDATE())) LIMIT 5;
-- SELECTの前にSETSEED()でシードを設定 -- SETSEED(-1.0 ~ 1.0 の範囲で指定) SELECT SETSEED(0.42); SELECT * FROM products ORDER BY RANDOM() LIMIT 10;
-- セッションレベルでシードを設定
BEGIN
DBMS_RANDOM.SEED(42);
END;
/
SELECT * FROM products
ORDER BY DBMS_RANDOM.VALUE
FETCH FIRST 10 ROWS ONLY;
-- 整数シードを指定(0 ~ 2^31-1) -- ただしNEWID()にシード指定はできないため -- CHECKSUM(RAND(42), col) を使う SELECT TOP 10 * FROM products ORDER BY CHECKSUM(RAND(42), id);
実践パターン集
パターン1:抽選システム(当選者をランダム選出)
-- エントリーテーブルから1名をランダム抽選
SELECT user_id, user_name, entry_at
FROM lottery_entries
WHERE campaign_id = 100
AND is_valid = 1
ORDER BY RAND()
LIMIT 1;
-- 当選者をトランザクション内で確定させる
BEGIN;
UPDATE lottery_entries
SET is_winner = 1
WHERE entry_id = (
SELECT entry_id
FROM (
SELECT entry_id FROM lottery_entries
WHERE campaign_id = 100 AND is_valid = 1
ORDER BY RAND() LIMIT 1
) t
);
COMMIT;
パターン2:クイズのランダム出題
-- カテゴリ「SQL基礎」から難易度別に3問ずつランダム出題
WITH ranked AS (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY difficulty
ORDER BY RAND()
) AS rn
FROM quiz_questions
WHERE category = 'SQL基礎'
AND is_active = 1
)
SELECT question_id, difficulty, question_text
FROM ranked
WHERE rn <= 3
ORDER BY difficulty, rn;
パターン3:A/Bテストのユーザー振り分け
同じユーザーIDに対して常に同じグループが返るよう、IDベースのハッシュ関数を使います。純粋なRAND()と違い、「セッションをまたいでも同じグループ」を保証できます。
-- MySQL: MOD(CRC32(user_id), 2) で0=A群 / 1=B群
SELECT
user_id,
CASE MOD(CRC32(user_id), 2)
WHEN 0 THEN 'A'
ELSE 'B'
END AS ab_group
FROM users;
-- PostgreSQL: MD5 + ハッシュで3グループ均等分割
SELECT
user_id,
CASE MOD(ABS(hashtext(user_id::text)), 3)
WHEN 0 THEN 'A'
WHEN 1 THEN 'B'
ELSE 'C'
END AS ab_group
FROM users;
パターン4:統計サンプリング(大規模データの品質チェック)
-- PostgreSQL: 100万件テーブルから1%をサンプリング
SELECT
COUNT(*) AS sample_count,
COUNT(email) AS email_filled,
COUNT(phone) AS phone_filled,
ROUND(100.0 * COUNT(email) / COUNT(*), 1) AS email_rate,
ROUND(100.0 * COUNT(phone) / COUNT(*), 1) AS phone_rate
FROM customers
TABLESAMPLE BERNOULLI (1);
-- Oracle: 5%をサンプリングして年代別件数を確認
SELECT
TRUNC(age / 10) * 10 AS age_group,
COUNT(*) AS cnt
FROM customers SAMPLE (5)
GROUP BY TRUNC(age / 10) * 10
ORDER BY age_group;
パターン5:ランダムなテストデータ生成
-- 名前・金額・日付をランダムに生成して100件INSERT
INSERT INTO orders (customer_name, amount, order_date)
SELECT
CONCAT('Customer_', n),
ROUND(1000 + RAND() * 99000), -- 1,000〜100,000
DATE_SUB(NOW(), INTERVAL FLOOR(RAND() * 365) DAY) -- 過去1年
FROM (
SELECT a.n + b.n * 10 AS n
FROM (SELECT 1 n UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5
UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 UNION SELECT 0) a
CROSS JOIN
(SELECT 0 n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3
UNION SELECT 4 UNION SELECT 5 UNION SELECT 6
UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) b
WHERE a.n + b.n * 10 BETWEEN 1 AND 100
) nums;
INSERT INTO orders (customer_name, amount, order_date)
SELECT
'Customer_' || n,
ROUND((1000 + RANDOM() * 99000)::numeric, 0),
CURRENT_DATE - (RANDOM() * 365)::int
FROM generate_series(1, 100) AS n;
各手法のパフォーマンス比較
| 手法 | 件数目安 | 均一性 | DBMS | 適用条件 |
|---|---|---|---|---|
| ORDER BY RAND() LIMIT N | 〜10万件 | ◎ | 全DBMS | 条件なし |
| WHERE id >= FLOOR(RAND() * MAX) | 100万件〜 | △ | MySQL中心 | IDが連番に近い |
| ROW_NUMBER() + 乱数JOIN | 10万〜100万件 | ◎ | 全DBMS | ウィンドウ関数が使える |
| TABLESAMPLE SYSTEM | 100万件〜 | △(ブロック依存) | PG / SS / Ora | 割合指定でOK |
| TABLESAMPLE BERNOULLI | 〜100万件 | ◎ | PostgreSQL | 割合指定でOK |
| SAMPLE句 | 100万件〜 | ○ | Oracle | 割合指定でOK |
小〜中規模(10万件未満)は
ORDER BY RAND()で十分。大規模テーブルかつ件数ちょうどN件が必要な場合はROW_NUMBER()方式。割合でよければTABLESAMPLE/SAMPLE句が最速です。よくある質問
ORDER BY RAND(42)のようにシード値を指定してください。PostgreSQLは事前にSELECT SETSEED(0.42)を実行します。WHERE category = 'electronics' ORDER BY RAND() LIMIT 10のように、WHERE句でフィルタしてからランダムソートできます。フィルタ後の件数が少なければパフォーマンスも問題になりにくいです。ORDER BY RAND() LIMIT Nは本来重複なしで取得します(異なる行が返る)。ただしIDベースの高速化パターンでは、JOINの仕方によっては重複が発生することがあります。その場合はGROUP BYやDISTINCTで重複を除去するか、ROW_NUMBER()方式に切り替えましょう。ORDER BY DBMS_RANDOM.VALUEと書くだけで使えます。詳細な数値範囲指定やシード設定・文字列生成などはOracle DBMS_RANDOM完全ガイドをご参照ください。TABLESAMPLE (10 PERCENT)でも8%〜12%程度の幅が出ることがあります。厳密にN件取得したい場合は、TABLESAMPLEの後にORDER BY RANDOM() LIMIT Nを付けるか、ORDER BY RAND()系の方法を使ってください。まとめ
SQLでランダムにデータを取得する方法は、要件と規模によって最適な手法が変わります。
- 小〜中規模(10万件未満):
ORDER BY RAND() LIMIT Nが最もシンプル - 大規模テーブルでN件ちょうど:ROW_NUMBER()方式でフルスキャンを回避
- 大規模テーブルで割合指定:TABLESAMPLE / SAMPLE句が高速
- 再現性が必要:シード付きRAND() / SETSEED() / TABLESAMPLE REPEATABLE
- A/Bテストの安定した振り分け:CRC32 / hashtext などのハッシュ関数を使う
関連記事:ORDER BY完全ガイド(ソート順・NULL順・パフォーマンス)、LIMIT句の使い方(OFFSET・ページネーション)、WITH句(CTE)完全ガイド、Oracle DBMS_RANDOM完全ガイド
