「MySQL - LIMIT句」の版間の差分

提供: MochiuWiki : SUSE, EC, PCB

文字列「__FORCETOC__」を「{{#seo: |title={{PAGENAME}} : Exploring Electronics and SUSE Linux | MochiuWiki |keywords=MochiuWiki,Mochiu,Wiki,Mochiu Wiki,Electric Circuit,Electric,pcb,Mathematics,AVR,TI,STMicro,AVR,ATmega,MSP430,STM,Arduino,Xilinx,FPGA,Verilog,HDL,PinePhone,Pine Phone,Raspberry,Raspberry Pi,C,C++,C#,Qt,Qml,MFC,Shell,Bash,Zsh,Fish,SUSE,SLE,Suse Enterprise,Suse Linux,openSUSE,open SUSE,Leap,Linux,uCLnux,Podman,電気回路,電子回路,基板,プリント基板 |description={{PAGENAME}} - 電子回路とSUSE Linuxに関する情報 | This pag…
編集の要約なし
 
1行目: 1行目:
== 概要 ==
== 概要 ==
<code>LIMIT</code>句は、MySQLのSQLクエリの一部として使用されており、取得するレコードの数を制限するためのものである。<br>
<code>LIMIT</code> 句は、MySQLのSQLクエリの一部として使用されており、取得するレコードの数を制限するためのものである。<br>
<br>
<br>
大量のレコードがあるテーブルから特定の数のレコードを取得する場合に使用する。<br>
大量のレコードがあるテーブルから特定の数のレコードを取得する場合に使用する。<br>
これにより、不要なレコードの読み込みを避けて、パフォーマンスを向上させることができる。<br>
これにより、不要なレコードの読み込みを避けて、パフォーマンスを向上させることができる。<br>
<br>
<code>LIMIT</code> 句は <code>SELECT</code> 文、<code>DELETE</code> 文で使用することができ、<code>OFFSET</code> 句と組み合わせることで、取得開始位置を指定することも可能である。<br>
<br>
ページネーション機能の実装においては、オフセットベース方式とカーソルベース方式の2つのアプローチが存在する。<br>
大規模なデータセットを扱う場合、適切なインデックス設計とページネーション方式の選択がパフォーマンスに大きく影響する。<br>
<br>
MySQL 8.0以降では、ウィンドウ関数との組み合わせにより、より高度なデータ取得が可能となっている。<br>
また、<code>ORDER BY</code> 句と組み合わせることにより、特定の順序でレコードを取得したり、ランダムなレコードを取得したりすることもできる。<br>
<br>
システム変数 <code>max_allowed_packet</code> の設定値にも注意が必要であり、大量のレコードを取得する場合には適切な値に調整する必要がある。<br>
<br><br>
<br><br>


== LIMITの基本構文 ==
== LIMITの基本構文 ==
<code>LIMIT</code>句は、<code>SELECT</code>文の最後に記述する。<br>
<code>LIMIT</code> 句は、<code>SELECT</code> 文の最後に記述する。<br>
  <syntaxhighlight lang="mysql">
  <syntaxhighlight lang="mysql">
  SELECT <* または カラム名> FROM <テーブル名> LIMIT <数値>;
  SELECT <* または カラム名> FROM <テーブル名> LIMIT <数値>;
15行目: 25行目:
  </syntaxhighlight>
  </syntaxhighlight>
<br>
<br>
例えば、ユーザにランダムな商品を表示する場合、<code>ORDER BY</code>句と<code>RAND()</code>関数と組み合わせて使用する。<br>
例えば、ユーザにランダムな商品を表示する場合、<code>ORDER BY</code> 句 と <code>RAND()</code> 関数と組み合わせて使用する。<br>
  <syntaxhighlight lang="mysql">
  <syntaxhighlight lang="mysql">
  SELECT * FROM <テーブル名> ORDER BY RAND() LIMIT <数値>;
  SELECT * FROM <テーブル名> ORDER BY RAND() LIMIT <数値>;
  </syntaxhighlight>
  </syntaxhighlight>
<br>
<br>
新しい順にレコードを取得する場合、<code>ORDER BY</code>句と<code>DESC</code>キーワードを組み合わせて使用する。<br>
新しい順にレコードを取得する場合、<code>ORDER BY</code> 句 と <code>DESC</code> キーワードを組み合わせて使用する。<br>
  <syntaxhighlight lang="mysql">
  <syntaxhighlight lang="mysql">
  SELECT * FROM <テーブル名> ORDER BY <カラム名> DESC LIMIT <数値>;
  SELECT * FROM <テーブル名> ORDER BY <カラム名> DESC LIMIT <数値>;
</syntaxhighlight>
<br>
<u>※注意</u><br>
<u><code>LIMIT</code>句のみを使用する場合、レコードの取得順序は保証されない。</u><br>
<u>特定の順序でレコードを取得する場合は、必ず<code>ORDER BY</code>句と組み合わせる必要がある。</u><br>
<br><br>
== OFFSET句 ==
<code>OFFSET</code> 句は、<code>LIMIT</code> 句と組み合わせて、取得開始位置を指定するために使用される。<br>
<br>
==== OFFSET構文 ====
<code>OFFSET</code> 句を使用することにより、指定した位置からレコードを取得することができる。<br>
<br>
<syntaxhighlight lang="mysql">
-- 標準構文
SELECT * FROM <テーブル名> LIMIT <row_count> OFFSET <offset>;
-- 省略構文
SELECT * FROM <テーブル名> LIMIT <offset>, <row_count>;
-- 例: 10件目から5件のレコードを取得
SELECT * FROM users LIMIT 5 OFFSET 10;
SELECT * FROM users LIMIT 10, 5;  -- 上と同じ結果
</syntaxhighlight>
<br>
<u>※注意</u><br>
<u>省略構文では、引数の順序が逆になることに注意が必要である。</u><br>
<br>
==== 引数の仕様 ====
<code>LIMIT</code>句と<code>OFFSET</code>句の引数には、以下に示す仕様が存在する。<br>
<br>
* 非負の整数定数を指定する必要がある。
* プリペアドステートメントでは、プレースホルダー (?) を使用することができる。
* ストアドプロシージャ内では、整数値の変数またはパラメータを使用することができる。
<br>
<syntaxhighlight lang="mysql">
-- プリペアドステートメントでの使用例
PREPARE stmt FROM 'SELECT * FROM users LIMIT ? OFFSET ?';
SET @limit = 10;
SET @offset = 20;
EXECUTE stmt USING @limit, @offset;
</syntaxhighlight>
<br>
==== 特定位置から全行を取得 ====
特定の位置から残りの全ての行を取得する場合は、大きな数値を指定する。<br>
<br>
<syntaxhighlight lang="mysql">
-- 95行目以降の全レコードを取得
SELECT * FROM users LIMIT 95, 18446744073709551615;
</syntaxhighlight>
<br>
==== LIMIT 0の用途 ====
<code>LIMIT 0</code> は、実際のデータを取得せずに、以下に示す用途で使用される。<br>
<br>
* クエリ構文の妥当性確認
* テーブルのメタデータ取得 (列名、データ型等)
* クエリの実行計画の確認
<br>
<syntaxhighlight lang="mysql">
-- メタデータのみを取得
SELECT * FROM users LIMIT 0;
  </syntaxhighlight>
  </syntaxhighlight>
<br><br>
<br><br>
29行目: 103行目:
特定のアプリケーションでは、ユーザがレコードを取得する場合に、一定の制限を設ける場合がある。<br>
特定のアプリケーションでは、ユーザがレコードを取得する場合に、一定の制限を設ける場合がある。<br>
これにより、以下に示すようなメリットがある。<br>
これにより、以下に示すようなメリットがある。<br>
<br>
* サーバへの過度な負荷を防ぐ。
* サーバへの過度な負荷を防ぐ。
* 情報漏洩を制限できる。
* 情報漏洩を制限できる。
* サービス拒否攻撃(DoS)の影響を軽減できる。
* サービス拒否攻撃 (DoS) の影響を軽減できる。
* ユーザエクスペリエンスの向上。
* ユーザエクスペリエンスの向上。
<br>
<br>
38行目: 113行目:


== MySQLの設定とLIMIT句 ==
== MySQLの設定とLIMIT句 ==
システム変数<code>max_allowed_packet</code>は、1度に送受信できるデータの最大サイズを制御することができる。<br>
システム変数 <code>max_allowed_packet</code> は、1度に送受信できるデータの最大サイズを制御することができる。<br>
<br>
<br>
ただし、この値が小さすぎる場合、<code>LIMIT</code>句を使用して大量のレコードを取得する時にエラーが発生する可能性がある。<br>
ただし、この値が小さすぎる場合、<code>LIMIT</code> 句を使用して大量のレコードを取得する時にエラーが発生する可能性がある。<br>
適切な値にすることにより、スムーズに大量のレコードを取得することができる。<br>
適切な値にすることにより、スムーズに大量のレコードを取得することができる。<br>
<br><br>
<br><br>


== 注意 ==
== ページネーションの実装 ==
<code>LIMIT</code>句を使用する場合、いくつかの注意点が存在する。<br>
ページネーションは、大量のデータを複数のページに分割して表示する手法である。<br>
MySQLでは、主に2つの方式が存在する。<br>
<br>
==== オフセットベースのページネーション ====
最も基本的なページネーション方式であり、<code>OFFSET</code> 句を使用して実装する。<br>
<br>
<syntaxhighlight lang="mysql">
-- 1ページ目 (0〜9件目)
SELECT * FROM products ORDER BY id LIMIT 10 OFFSET 0;
-- 2ページ目 (10〜19件目)
SELECT * FROM products ORDER BY id LIMIT 10 OFFSET 10;
-- 3ページ目 (20〜29件目)
SELECT * FROM products ORDER BY id LIMIT 10 OFFSET 20;
</syntaxhighlight>
<br>
<u>※注意</u><br>
<u>この方式は、大きな<code>OFFSET</code>値を使用する場合にパフォーマンスが低下する。</u><br>
<br>
==== 決定論的なソートの重要性 ====
ページネーションを実装する場合、決定論的なソート (常に同じ順序を返すソート) が重要である。<br>
<br>
複数の行が同じ値を持つ場合、<code>ORDER BY</code> 句の結果は不確定になる可能性がある。<br>
これにより、ページをまたいで同じレコードが表示されたり、レコードが欠落したりする問題が発生する。<br>
<br>
<syntaxhighlight lang="mysql">
-- 不確定なソート (複数のユーザが同じcreated_atを持つ場合)
SELECT * FROM users ORDER BY created_at LIMIT 10 OFFSET 0;
-- 決定論的なソート (一意性を保証)
SELECT * FROM users ORDER BY created_at, id LIMIT 10 OFFSET 0;
</syntaxhighlight>
<br>
主キーや一意なカラムを <code>ORDER BY</code> 句に追加することにより、決定論的なソートを実現できる。<br>
<br>
==== カーソルベースのページネーション (キーセット方式) ====
カーソルベースのページネーションは、<code>OFFSET</code> 句を使用せず、<code>WHERE</code> 条件でカーソル位置を指定する方式である。<br>
<br>
この方式は、大規模なデータセットにおいて高いパフォーマンスを発揮する。<br>
<br>
<syntaxhighlight lang="mysql">
-- 1ページ目
SELECT * FROM products ORDER BY id LIMIT 10;
-- 2ページ目 (最後に取得したidが100の場合)
SELECT * FROM products WHERE id > 100 ORDER BY id LIMIT 10;
-- 3ページ目 (最後に取得したidが110の場合)
SELECT * FROM products WHERE id > 110 ORDER BY id LIMIT 10;
</syntaxhighlight>
<br>
===== カーソルベース方式の利点 =====
* O(log N) の時間計算量であり、大規模なデータセットでも高速である。
* 行の削除に強い。(他のユーザがレコードを削除しても、ページのずれが発生しにくい)
* インデックスを効果的に活用できる。
<br>
===== カーソルベース方式の欠点 =====
* 特定のページ番号に直接アクセスすることができない。
* 前のページに戻ることが複雑になる。
* 実装がやや複雑である。
<br>
===== 複数列でのカーソルベースページネーション =====
複数の列でソートする場合のカーソルベースページネーションの実装例を以下に示す。<br>
<br>
<syntaxhighlight lang="mysql">
-- created_atとidでソートする場合
-- 1ページ目
SELECT * FROM users ORDER BY created_at DESC, id DESC LIMIT 10;
-- 2ページ目 (最後に取得したレコードがcreated_at='2024-01-01', id=100の場合)
SELECT * FROM users
    WHERE (created_at < '2024-01-01')
      OR (created_at = '2024-01-01' AND id < 100)
    ORDER BY created_at DESC, id DESC
    LIMIT 10;
</syntaxhighlight>
<br>
==== デファードジョイン方式 ====
デファードジョイン方式は、オフセットベースとカーソルベースの利点を組み合わせた手法である。<br>
<br>
この方式では、まずインデックスを使用して必要な主キーのみを取得し、その後にJOINで全てのカラムを取得する。<br>
<br>
<syntaxhighlight lang="mysql">
-- 通常のオフセットベース (非効率)
SELECT * FROM large_table ORDER BY created_at LIMIT 10 OFFSET 100000;
-- デファードジョイン方式 (効率的)
SELECT t.*
FROM large_table t
INNER JOIN (
    SELECT id
    FROM large_table
    ORDER BY created_at
    LIMIT 10 OFFSET 100000
) AS subquery USING (id);
</syntaxhighlight>
<br>
この方式により、大きな <code>OFFSET</code> 値を使用する場合でも、パフォーマンスの低下を抑えることができる。<br>
<br><br>
 
== パフォーマンス最適化 ==
<code>LIMIT</code> 句を使用する場合、パフォーマンスを最適化するための手法が存在する。<br>
<br>
==== 大きなOFFSET値の問題点 ====
<code>OFFSET</code> 句に大きな値を指定する場合、MySQLは指定された位置までの全ての行を読み取る必要がある。<br>
<br>
これにより、時間計算量がO(N)となり、<code>OFFSET</code> 値が大きいほどパフォーマンスが低下する。<br>
<br>
<syntaxhighlight lang="mysql">
-- 100万件目から10件を取得する場合、
-- MySQLは100万件を読み取ってから結果を返す
SELECT * FROM large_table ORDER BY id LIMIT 10 OFFSET 1000000;
</syntaxhighlight>
<br>
このような場合、カーソルベースのページネーションやデファードジョイン方式を使用することが推奨される。<br>
<br>
==== カバリングインデックスの活用 ====
カバリングインデックスは、クエリで必要な全てのカラムを含むインデックスである。<br>
<br>
カバリングインデックスを使用することにより、テーブルデータにアクセスせずにインデックスのみでクエリを処理できる。<br>
<br>
<syntaxhighlight lang="mysql">
-- カバリングインデックスを作成
CREATE INDEX idx_covering ON users (status, created_at, id, name);
-- カバリングインデックスを活用したクエリ
SELECT id, name, created_at
FROM users
WHERE status = 'active'
ORDER BY created_at DESC
LIMIT 10;
</syntaxhighlight>
<br>
<code>EXPLAIN</code> コマンドを使用して、クエリがカバリングインデックスを使用しているかを確認できる。<br>
<br>
==== ORDER BY + LIMIT最適化 ====
MySQLは、<code>ORDER BY</code> 句 と <code>LIMIT</code> 句を組み合わせたクエリに対して、特別な最適化を行う。<br>
<br>
適切なインデックスが存在する場合、MySQLは全ての行をソートせずに、必要な数の行のみを処理する。<br>
<br>
<syntaxhighlight lang="mysql">
-- インデックスを作成
CREATE INDEX idx_created_at ON users (created_at);
-- 最適化されたクエリ
SELECT * FROM users ORDER BY created_at DESC LIMIT 10;
</syntaxhighlight>
<br>
この最適化により、大規模なテーブルでも高速にクエリを実行できる。<br>
<br>
==== InnoDBでの主キーの自動追加 ====
InnoDBストレージエンジンでは、<code>ORDER BY</code> 句に主キーが自動的に追加される場合がある。<br>
<br>
これにより、決定論的なソートが自動的に保証されることがある。<br>
ただし、この動作に依存せず、明示的に主キーを指定することが推奨される。<br>
<br>
<syntaxhighlight lang="mysql">
-- 明示的に主キーを指定
SELECT * FROM users ORDER BY status, id LIMIT 10;
</syntaxhighlight>
<br><br>
 
== サブクエリ・JOIN・UNIONでのLIMIT句 ==
<code>LIMIT</code> 句は、サブクエリ、<code>JOIN</code>、<code>UNION</code> と組み合わせて使用することができる。<br>
<br>
==== サブクエリでのLIMIT ====
サブクエリ内で <code>LIMIT</code> 句を使用することにより、サブクエリの結果を制限できる。<br>
<br>
<syntaxhighlight lang="mysql">
-- サブクエリで最新の10件の注文を取得し、その合計金額を計算
SELECT SUM(amount) AS total
FROM (
    SELECT amount
    FROM orders
    ORDER BY created_at DESC
    LIMIT 10
) AS recent_orders;
</syntaxhighlight>
<br>
==== JOINでのLIMIT使用時の注意 ====
<code>JOIN</code> を使用する場合、<code>LIMIT</code> 句は結合後の結果に適用される。<br>
<br>
<syntaxhighlight lang="mysql">
-- JOINした後の結果から10件を取得
SELECT users.name, orders.amount
FROM users
INNER JOIN orders ON users.id = orders.user_id
ORDER BY orders.created_at DESC
LIMIT 10;
</syntaxhighlight>
<br>
各テーブルから10件ずつ取得する場合は、サブクエリを使用する必要がある。<br>
<br>
<syntaxhighlight lang="mysql">
-- 各ユーザの最新10件の注文を取得
SELECT users.name, recent_orders.amount
FROM users
INNER JOIN (
    SELECT user_id, amount, created_at
    FROM orders
    ORDER BY created_at DESC
    LIMIT 10
) AS recent_orders ON users.id = recent_orders.user_id;
</syntaxhighlight>
<br>
==== UNIONでのLIMIT ====
<code>UNION</code> を使用する場合、<code>LIMIT</code> 句は個別の <code>SELECT</code> 文または全体の結果に適用できる。<br>
<br>
<syntaxhighlight lang="mysql">
-- 各SELECTで10件ずつ取得し、結果を結合
(SELECT * FROM users WHERE status = 'active' ORDER BY created_at DESC LIMIT 10)
UNION ALL
(SELECT * FROM users WHERE status = 'inactive' ORDER BY created_at DESC LIMIT 10);
-- 結合後の全体から10件を取得
(SELECT * FROM users WHERE status = 'active')
UNION ALL
(SELECT * FROM users WHERE status = 'inactive')
ORDER BY created_at DESC
LIMIT 10;
</syntaxhighlight>
<br>
<u>※注意</u><br>
<u>個別の <code>SELECT</code> 文に <code>LIMIT</code> 句を適用する場合は、括弧で囲む必要がある。</u><br>
<br><br>
 
== UPDATE文 / DELETE文でのLIMIT句 ==
<code>LIMIT</code> 句は、<code>DELETE</code> 文で使用することができるが、<code>UPDATE</code> 文では直接使用することができない。<br>
<br>
==== DELETE文でのLIMIT ====
<code>DELETE</code> 文で <code>LIMIT</code> 句を使用することにより、削除する行数を制限できる。<br>
<br>
<syntaxhighlight lang="mysql">
-- 古いログレコードを10件削除
DELETE FROM logs
ORDER BY created_at ASC
LIMIT 10;
</syntaxhighlight>
<br>
==== バッチ削除パターン ====
大量のレコードを削除する場合、一度に削除せずにバッチで削除することが推奨される。<br>
<br>
これにより、テーブルロックの時間を短縮し、レプリケーションへの影響を最小限に抑えることができる。<br>
<br>
<syntaxhighlight lang="mysql">
-- 古いログを1000件ずつ削除
DELIMITER $$
CREATE PROCEDURE delete_old_logs()
BEGIN
    DECLARE rows_affected INT DEFAULT 1;
    WHILE rows_affected > 0 DO
      DELETE FROM logs
      WHERE created_at < DATE_SUB(NOW(), INTERVAL 90 DAY)
      ORDER BY created_at ASC
      LIMIT 1000;
      SET rows_affected = ROW_COUNT();
      -- レプリケーションの負荷を軽減するために短い待機時間を設ける
      DO SLEEP(0.1);
    END WHILE;
END$$
DELIMITER ;
</syntaxhighlight>
<br>
==== 制限事項 ====
<code>LIMIT</code> 句を <code>DELETE</code> 文で使用する場合、以下に示す制限事項が存在する。<br>
<br>
* 単一テーブルのみサポートされる。(マルチテーブル削除では使用不可)
* サブクエリとの併用には制限がある。
<br>
<syntaxhighlight lang="mysql">
-- エラー: マルチテーブル削除ではLIMITを使用できない
DELETE users, orders
FROM users
INNER JOIN orders ON users.id = orders.user_id
WHERE users.status = 'deleted'
LIMIT 10;  -- エラー
</syntaxhighlight>
<br>
<br>
* 順序の考慮
==== UPDATE文での代替方法 ====
*: <code>LIMIT</code>句のみを使用する場合、レコードはテーブルに保存された順序で取得される。
<code>UPDATE</code> 文では <code>LIMIT</code> 句を直接使用できないが、サブクエリを使用することで同様の効果を得られる。<br>
*: 特定の順序でレコードを取得する場合、<code>ORDER BY</code>句と組み合わせる必要がある。
<br>
*: <br>
<syntaxhighlight lang="mysql">
* ページネーションの実装
-- サブクエリを使用して最初の10件を更新
*: <code>LIMIT</code>句を使用してページネーションを実装する場合、OFFSETも同時に使用する。
*: これにより、表示するレコードの開始位置を指定することができる。
UPDATE users
*: <br>
SET status = 'processed'
* パフォーマンスの影響
WHERE id IN (
*: 大量のレコードを持つテーブルにおいて<code>LIMIT</code>句を使用する場合、パフォーマンスに影響が出ることがある。
    SELECT id FROM (
*: この点も考慮して、必要に応じてインデックス等の最適化を行うことが推奨される。
      SELECT id
      FROM users
      WHERE status = 'pending'
      ORDER BY created_at ASC
      LIMIT 10
    ) AS tmp
);
</syntaxhighlight>
<br>
<u>※注意</u><br>
<u>サブクエリを2重にネストしているのは、MySQLの制限により、更新対象のテーブルを直接サブクエリで参照できないためである。</u><br>
<br><br>
<br><br>
== MySQL 8.0以降の機能 ==
MySQL 8.0では、ウィンドウ関数が導入され、<code>LIMIT</code> 句と組み合わせることで高度なデータ取得が可能になった。<br>
<br>
==== ウィンドウ関数との組み合わせ ====
ウィンドウ関数を使用することにより、各グループ内での順位付けや集計を行うことができる。<br>
<br>
<syntaxhighlight lang="mysql">
-- ウィンドウ関数で順位を付けてから上位10件を取得
SELECT *
FROM (
    SELECT
      id,
      name,
      department,
      salary,
      RANK() OVER (ORDER BY salary DESC) AS salary_rank
    FROM employees
) AS ranked_employees
WHERE salary_rank <= 10;
</syntaxhighlight>
<br>
==== ROW_NUMBER関数を使用したページネーション ====
<code>ROW_NUMBER</code> 関数を使用することにより、決定論的なページネーションを定義できる。<br>
<br>
この方式は、決定論的なソートを保証しつつ、特定のページ範囲を取得することができる。<br>
<br>
<syntaxhighlight lang="mysql">
-- ROW_NUMBER関数を使用したページネーション
SELECT *
FROM (
    SELECT
      id,
      name,
      created_at,
      ROW_NUMBER() OVER (ORDER BY created_at DESC, id DESC) AS row_num
    FROM users
) AS numbered_users
WHERE row_num BETWEEN 11 AND 20;  -- 2ページ目 (11〜20件目)
</syntaxhighlight>
<br>
==== RANK()、DENSE_RANK()の使用 ====
<code>RANK</code> 関数 や <code>DENSE_RANK</code> 関数を使用することにより、同順位を考慮したランキングを実装できる。<br>
<br>
<syntaxhighlight lang="mysql">
-- 各部門の上位5名を取得
SELECT *
FROM (
    SELECT
      id,
      name,
      department,
      salary,
      RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dept_rank
    FROM employees
) AS ranked_employees
WHERE dept_rank <= 5;
</syntaxhighlight>
<br>
==== PARTITION BYの活用 ====
<code>PARTITION BY</code> 句を使用することにより、グループごとに <code>LIMIT</code> のような制限を実現できる。<br>
<br>
<syntaxhighlight lang="mysql">
-- 各カテゴリの最新3件の商品を取得
SELECT *
FROM (
    SELECT
      id,
      name,
      category,
      created_at,
      ROW_NUMBER() OVER (PARTITION BY category ORDER BY created_at DESC) AS row_num
    FROM products
) AS numbered_products
WHERE row_num <= 3;
</syntaxhighlight>
<br>
==== FETCH FIRST構文の非サポート ====
SQL標準の <code>FETCH FIRST</code> 構文は、MySQL 8.0でもサポートされていない。<br>
<br>
<syntaxhighlight lang="mysql">
-- エラー : MySQLではFETCH FIRST構文はサポートされていない
SELECT * FROM users
ORDER BY created_at DESC
FETCH FIRST 10 ROWS ONLY;  -- エラー
-- MySQLでは以下を使用する
SELECT * FROM users
ORDER BY created_at DESC
LIMIT 10;
</syntaxhighlight>
<br><br>
== ROW_COUNT関数との関係 ==
<code>ROW_COUNT</code> 関数は、直前のSQL文が影響を与えた行数を返す関数である。<br>
<br>
==== ROW_COUNT関数の動作 ====
<code>ROW_COUNT</code> 関数は、<code>INSERT</code>、<code>UPDATE</code>、<code>DELETE</code> 文で影響を受けた行数を返す。<br>
<br>
<syntaxhighlight lang="mysql">
-- DELETE文で削除された行数を確認
DELETE FROM logs
WHERE created_at < DATE_SUB(NOW(), INTERVAL 90 DAY)
LIMIT 1000;
SELECT ROW_COUNT();  -- 削除された行数が返される
</syntaxhighlight>
<br>
==== SELECT文では-1を返す ====
<code>SELECT</code> 文を実行した場合、<code>ROW_COUNT</code> 関数は <u>-1</u> を返す。<br>
<br>
<code>SELECT</code> 文の結果行数を取得する場合は、<code>FOUND_ROWS</code> 関数を使用する必要があるが、これはMySQL 8.0で非推奨となっている。<br>
<br>
<syntaxhighlight lang="mysql">
SELECT * FROM users LIMIT 10;
SELECT ROW_COUNT();  -- -1が返される
</syntaxhighlight>
<br>
==== バッチ処理での活用 ====
<code>ROW_COUNT</code> 関数は、バッチ削除やバッチ更新の処理で活用できる。<br>
<br>
<syntaxhighlight lang="mysql">
-- バッチ削除の例
DELIMITER $$
CREATE PROCEDURE batch_delete()
BEGIN
    DECLARE affected_rows INT;
    DECLARE total_deleted INT DEFAULT 0;
    REPEAT
      DELETE FROM logs
      WHERE created_at < DATE_SUB(NOW(), INTERVAL 90 DAY)
      LIMIT 1000;
      SET affected_rows = ROW_COUNT();
      SET total_deleted = total_deleted + affected_rows;
      DO SLEEP(0.1);
    UNTIL affected_rows = 0 END REPEAT;
    SELECT total_deleted AS total_deleted_rows;
END$$
DELIMITER ;
</syntaxhighlight>
<br>
==== SQL_CALC_FOUND_ROWS、FOUND_ROWS関数の非推奨 ====
MySQL 8.0.17以降、<code>SQL_CALC_FOUND_ROWS</code> クエリ修飾子 と <code>FOUND_ROWS</code> 関数は非推奨となった。<br>
<br>
<syntaxhighlight lang="mysql">
-- 非推奨 : SQL_CALC_FOUND_ROWSの使用
SELECT SQL_CALC_FOUND_ROWS * FROM users LIMIT 10;
SELECT FOUND_ROWS();  -- 全体の行数を返す (非推奨)
</syntaxhighlight>
<br>
代わりに、<code>COUNT(*)</code> クエリを別途実行することが推奨される。<br>
<br>
<syntaxhighlight lang="mysql">
-- 推奨 : COUNT(*)を使用
 
SELECT COUNT(*) AS total FROM users WHERE status = 'active';
SELECT * FROM users WHERE status = 'active' LIMIT 10;
</syntaxhighlight>
<br><br>


{{#seo:
{{#seo:

2026年2月5日 (木) 14:04時点における最新版

概要

LIMIT 句は、MySQLのSQLクエリの一部として使用されており、取得するレコードの数を制限するためのものである。

大量のレコードがあるテーブルから特定の数のレコードを取得する場合に使用する。
これにより、不要なレコードの読み込みを避けて、パフォーマンスを向上させることができる。

LIMIT 句は SELECT 文、DELETE 文で使用することができ、OFFSET 句と組み合わせることで、取得開始位置を指定することも可能である。

ページネーション機能の実装においては、オフセットベース方式とカーソルベース方式の2つのアプローチが存在する。
大規模なデータセットを扱う場合、適切なインデックス設計とページネーション方式の選択がパフォーマンスに大きく影響する。

MySQL 8.0以降では、ウィンドウ関数との組み合わせにより、より高度なデータ取得が可能となっている。
また、ORDER BY 句と組み合わせることにより、特定の順序でレコードを取得したり、ランダムなレコードを取得したりすることもできる。

システム変数 max_allowed_packet の設定値にも注意が必要であり、大量のレコードを取得する場合には適切な値に調整する必要がある。


LIMITの基本構文

LIMIT 句は、SELECT 文の最後に記述する。

 SELECT <* または カラム名> FROM <テーブル名> LIMIT <数値>;
 
 -- 例: hogeテーブルから最初の5件のレコードを取得する
 SELECT * FROM hoge LIMIT 5;


例えば、ユーザにランダムな商品を表示する場合、ORDER BY 句 と RAND() 関数と組み合わせて使用する。

 SELECT * FROM <テーブル名> ORDER BY RAND() LIMIT <数値>;


新しい順にレコードを取得する場合、ORDER BY 句 と DESC キーワードを組み合わせて使用する。

 SELECT * FROM <テーブル名> ORDER BY <カラム名> DESC LIMIT <数値>;


※注意
LIMIT句のみを使用する場合、レコードの取得順序は保証されない。
特定の順序でレコードを取得する場合は、必ずORDER BY句と組み合わせる必要がある。


OFFSET句

OFFSET 句は、LIMIT 句と組み合わせて、取得開始位置を指定するために使用される。

OFFSET構文

OFFSET 句を使用することにより、指定した位置からレコードを取得することができる。

 -- 標準構文
 SELECT * FROM <テーブル名> LIMIT <row_count> OFFSET <offset>;
 
 -- 省略構文
 SELECT * FROM <テーブル名> LIMIT <offset>, <row_count>;
 
 -- 例: 10件目から5件のレコードを取得
 SELECT * FROM users LIMIT 5 OFFSET 10;
 SELECT * FROM users LIMIT 10, 5;  -- 上と同じ結果


※注意
省略構文では、引数の順序が逆になることに注意が必要である。

引数の仕様

LIMIT句とOFFSET句の引数には、以下に示す仕様が存在する。

  • 非負の整数定数を指定する必要がある。
  • プリペアドステートメントでは、プレースホルダー (?) を使用することができる。
  • ストアドプロシージャ内では、整数値の変数またはパラメータを使用することができる。


 -- プリペアドステートメントでの使用例
 
 PREPARE stmt FROM 'SELECT * FROM users LIMIT ? OFFSET ?';
 SET @limit = 10;
 SET @offset = 20;
 EXECUTE stmt USING @limit, @offset;


特定位置から全行を取得

特定の位置から残りの全ての行を取得する場合は、大きな数値を指定する。

 -- 95行目以降の全レコードを取得
 
 SELECT * FROM users LIMIT 95, 18446744073709551615;


LIMIT 0の用途

LIMIT 0 は、実際のデータを取得せずに、以下に示す用途で使用される。

  • クエリ構文の妥当性確認
  • テーブルのメタデータ取得 (列名、データ型等)
  • クエリの実行計画の確認


 -- メタデータのみを取得
 
 SELECT * FROM users LIMIT 0;



レコード取得の制限

特定のアプリケーションでは、ユーザがレコードを取得する場合に、一定の制限を設ける場合がある。
これにより、以下に示すようなメリットがある。

  • サーバへの過度な負荷を防ぐ。
  • 情報漏洩を制限できる。
  • サービス拒否攻撃 (DoS) の影響を軽減できる。
  • ユーザエクスペリエンスの向上。


必要な数のみレコードを取得することにより、サーバのリソースを効果的に使用できる。


MySQLの設定とLIMIT句

システム変数 max_allowed_packet は、1度に送受信できるデータの最大サイズを制御することができる。

ただし、この値が小さすぎる場合、LIMIT 句を使用して大量のレコードを取得する時にエラーが発生する可能性がある。
適切な値にすることにより、スムーズに大量のレコードを取得することができる。


ページネーションの実装

ページネーションは、大量のデータを複数のページに分割して表示する手法である。
MySQLでは、主に2つの方式が存在する。

オフセットベースのページネーション

最も基本的なページネーション方式であり、OFFSET 句を使用して実装する。

 -- 1ページ目 (0〜9件目)
 SELECT * FROM products ORDER BY id LIMIT 10 OFFSET 0;
 
 -- 2ページ目 (10〜19件目)
 SELECT * FROM products ORDER BY id LIMIT 10 OFFSET 10;
 
 -- 3ページ目 (20〜29件目)
 SELECT * FROM products ORDER BY id LIMIT 10 OFFSET 20;


※注意
この方式は、大きなOFFSET値を使用する場合にパフォーマンスが低下する。

決定論的なソートの重要性

ページネーションを実装する場合、決定論的なソート (常に同じ順序を返すソート) が重要である。

複数の行が同じ値を持つ場合、ORDER BY 句の結果は不確定になる可能性がある。
これにより、ページをまたいで同じレコードが表示されたり、レコードが欠落したりする問題が発生する。

 -- 不確定なソート (複数のユーザが同じcreated_atを持つ場合)
 SELECT * FROM users ORDER BY created_at LIMIT 10 OFFSET 0;
 
 -- 決定論的なソート (一意性を保証)
 SELECT * FROM users ORDER BY created_at, id LIMIT 10 OFFSET 0;


主キーや一意なカラムを ORDER BY 句に追加することにより、決定論的なソートを実現できる。

カーソルベースのページネーション (キーセット方式)

カーソルベースのページネーションは、OFFSET 句を使用せず、WHERE 条件でカーソル位置を指定する方式である。

この方式は、大規模なデータセットにおいて高いパフォーマンスを発揮する。

 -- 1ページ目
 SELECT * FROM products ORDER BY id LIMIT 10;
 
 -- 2ページ目 (最後に取得したidが100の場合)
 SELECT * FROM products WHERE id > 100 ORDER BY id LIMIT 10;
 
 -- 3ページ目 (最後に取得したidが110の場合)
 SELECT * FROM products WHERE id > 110 ORDER BY id LIMIT 10;


カーソルベース方式の利点
  • O(log N) の時間計算量であり、大規模なデータセットでも高速である。
  • 行の削除に強い。(他のユーザがレコードを削除しても、ページのずれが発生しにくい)
  • インデックスを効果的に活用できる。


カーソルベース方式の欠点
  • 特定のページ番号に直接アクセスすることができない。
  • 前のページに戻ることが複雑になる。
  • 実装がやや複雑である。


複数列でのカーソルベースページネーション

複数の列でソートする場合のカーソルベースページネーションの実装例を以下に示す。

 -- created_atとidでソートする場合
 
 -- 1ページ目
 SELECT * FROM users ORDER BY created_at DESC, id DESC LIMIT 10;
 
 -- 2ページ目 (最後に取得したレコードがcreated_at='2024-01-01', id=100の場合)
 SELECT * FROM users
    WHERE (created_at < '2024-01-01')
       OR (created_at = '2024-01-01' AND id < 100)
    ORDER BY created_at DESC, id DESC
    LIMIT 10;


デファードジョイン方式

デファードジョイン方式は、オフセットベースとカーソルベースの利点を組み合わせた手法である。

この方式では、まずインデックスを使用して必要な主キーのみを取得し、その後にJOINで全てのカラムを取得する。

 -- 通常のオフセットベース (非効率)
 SELECT * FROM large_table ORDER BY created_at LIMIT 10 OFFSET 100000;
 
 -- デファードジョイン方式 (効率的)
 SELECT t.*
 FROM large_table t
 INNER JOIN (
    SELECT id
    FROM large_table
    ORDER BY created_at
    LIMIT 10 OFFSET 100000
 ) AS subquery USING (id);


この方式により、大きな OFFSET 値を使用する場合でも、パフォーマンスの低下を抑えることができる。


パフォーマンス最適化

LIMIT 句を使用する場合、パフォーマンスを最適化するための手法が存在する。

大きなOFFSET値の問題点

OFFSET 句に大きな値を指定する場合、MySQLは指定された位置までの全ての行を読み取る必要がある。

これにより、時間計算量がO(N)となり、OFFSET 値が大きいほどパフォーマンスが低下する。

 -- 100万件目から10件を取得する場合、
 -- MySQLは100万件を読み取ってから結果を返す
 
 SELECT * FROM large_table ORDER BY id LIMIT 10 OFFSET 1000000;


このような場合、カーソルベースのページネーションやデファードジョイン方式を使用することが推奨される。

カバリングインデックスの活用

カバリングインデックスは、クエリで必要な全てのカラムを含むインデックスである。

カバリングインデックスを使用することにより、テーブルデータにアクセスせずにインデックスのみでクエリを処理できる。

 -- カバリングインデックスを作成
 CREATE INDEX idx_covering ON users (status, created_at, id, name);
 
 -- カバリングインデックスを活用したクエリ
 SELECT id, name, created_at
 FROM users
 WHERE status = 'active'
 ORDER BY created_at DESC
 LIMIT 10;


EXPLAIN コマンドを使用して、クエリがカバリングインデックスを使用しているかを確認できる。

ORDER BY + LIMIT最適化

MySQLは、ORDER BY 句 と LIMIT 句を組み合わせたクエリに対して、特別な最適化を行う。

適切なインデックスが存在する場合、MySQLは全ての行をソートせずに、必要な数の行のみを処理する。

 -- インデックスを作成
 CREATE INDEX idx_created_at ON users (created_at);
 
 -- 最適化されたクエリ
 SELECT * FROM users ORDER BY created_at DESC LIMIT 10;


この最適化により、大規模なテーブルでも高速にクエリを実行できる。

InnoDBでの主キーの自動追加

InnoDBストレージエンジンでは、ORDER BY 句に主キーが自動的に追加される場合がある。

これにより、決定論的なソートが自動的に保証されることがある。
ただし、この動作に依存せず、明示的に主キーを指定することが推奨される。

 -- 明示的に主キーを指定
 
 SELECT * FROM users ORDER BY status, id LIMIT 10;



サブクエリ・JOIN・UNIONでのLIMIT句

LIMIT 句は、サブクエリ、JOINUNION と組み合わせて使用することができる。

サブクエリでのLIMIT

サブクエリ内で LIMIT 句を使用することにより、サブクエリの結果を制限できる。

 -- サブクエリで最新の10件の注文を取得し、その合計金額を計算
 
 SELECT SUM(amount) AS total
 FROM (
    SELECT amount
    FROM orders
    ORDER BY created_at DESC
    LIMIT 10
 ) AS recent_orders;


JOINでのLIMIT使用時の注意

JOIN を使用する場合、LIMIT 句は結合後の結果に適用される。

 -- JOINした後の結果から10件を取得
 
 SELECT users.name, orders.amount
 FROM users
 INNER JOIN orders ON users.id = orders.user_id
 ORDER BY orders.created_at DESC
 LIMIT 10;


各テーブルから10件ずつ取得する場合は、サブクエリを使用する必要がある。

 -- 各ユーザの最新10件の注文を取得
 
 SELECT users.name, recent_orders.amount
 FROM users
 INNER JOIN (
    SELECT user_id, amount, created_at
    FROM orders
    ORDER BY created_at DESC
    LIMIT 10
 ) AS recent_orders ON users.id = recent_orders.user_id;


UNIONでのLIMIT

UNION を使用する場合、LIMIT 句は個別の SELECT 文または全体の結果に適用できる。

 -- 各SELECTで10件ずつ取得し、結果を結合
 
 (SELECT * FROM users WHERE status = 'active' ORDER BY created_at DESC LIMIT 10)
 UNION ALL
 (SELECT * FROM users WHERE status = 'inactive' ORDER BY created_at DESC LIMIT 10);
 
 -- 結合後の全体から10件を取得
 (SELECT * FROM users WHERE status = 'active')
 UNION ALL
 (SELECT * FROM users WHERE status = 'inactive')
 ORDER BY created_at DESC
 LIMIT 10;


※注意
個別の SELECT 文に LIMIT 句を適用する場合は、括弧で囲む必要がある。


UPDATE文 / DELETE文でのLIMIT句

LIMIT 句は、DELETE 文で使用することができるが、UPDATE 文では直接使用することができない。

DELETE文でのLIMIT

DELETE 文で LIMIT 句を使用することにより、削除する行数を制限できる。

 -- 古いログレコードを10件削除
 
 DELETE FROM logs
 ORDER BY created_at ASC
 LIMIT 10;


バッチ削除パターン

大量のレコードを削除する場合、一度に削除せずにバッチで削除することが推奨される。

これにより、テーブルロックの時間を短縮し、レプリケーションへの影響を最小限に抑えることができる。

 -- 古いログを1000件ずつ削除
 
 DELIMITER $$
 
 CREATE PROCEDURE delete_old_logs()
 BEGIN
    DECLARE rows_affected INT DEFAULT 1;
 
    WHILE rows_affected > 0 DO
       DELETE FROM logs
       WHERE created_at < DATE_SUB(NOW(), INTERVAL 90 DAY)
       ORDER BY created_at ASC
       LIMIT 1000;
 
       SET rows_affected = ROW_COUNT();
 
       -- レプリケーションの負荷を軽減するために短い待機時間を設ける
       DO SLEEP(0.1);
    END WHILE;
 END$$
 
 DELIMITER ;


制限事項

LIMIT 句を DELETE 文で使用する場合、以下に示す制限事項が存在する。

  • 単一テーブルのみサポートされる。(マルチテーブル削除では使用不可)
  • サブクエリとの併用には制限がある。


 -- エラー: マルチテーブル削除ではLIMITを使用できない
 
 DELETE users, orders
 FROM users
 INNER JOIN orders ON users.id = orders.user_id
 WHERE users.status = 'deleted'
 LIMIT 10;  -- エラー


UPDATE文での代替方法

UPDATE 文では LIMIT 句を直接使用できないが、サブクエリを使用することで同様の効果を得られる。

 -- サブクエリを使用して最初の10件を更新
 
 UPDATE users
 SET status = 'processed'
 WHERE id IN (
    SELECT id FROM (
       SELECT id
       FROM users
       WHERE status = 'pending'
       ORDER BY created_at ASC
       LIMIT 10
    ) AS tmp
 );


※注意
サブクエリを2重にネストしているのは、MySQLの制限により、更新対象のテーブルを直接サブクエリで参照できないためである。


MySQL 8.0以降の機能

MySQL 8.0では、ウィンドウ関数が導入され、LIMIT 句と組み合わせることで高度なデータ取得が可能になった。

ウィンドウ関数との組み合わせ

ウィンドウ関数を使用することにより、各グループ内での順位付けや集計を行うことができる。

 -- ウィンドウ関数で順位を付けてから上位10件を取得
 
 SELECT *
 FROM (
    SELECT
       id,
       name,
       department,
       salary,
       RANK() OVER (ORDER BY salary DESC) AS salary_rank
    FROM employees
 ) AS ranked_employees
 WHERE salary_rank <= 10;


ROW_NUMBER関数を使用したページネーション

ROW_NUMBER 関数を使用することにより、決定論的なページネーションを定義できる。

この方式は、決定論的なソートを保証しつつ、特定のページ範囲を取得することができる。

 -- ROW_NUMBER関数を使用したページネーション
 
 SELECT *
 FROM (
    SELECT
       id,
       name,
       created_at,
       ROW_NUMBER() OVER (ORDER BY created_at DESC, id DESC) AS row_num
    FROM users
 ) AS numbered_users
 WHERE row_num BETWEEN 11 AND 20;  -- 2ページ目 (11〜20件目)


RANK()、DENSE_RANK()の使用

RANK 関数 や DENSE_RANK 関数を使用することにより、同順位を考慮したランキングを実装できる。

 -- 各部門の上位5名を取得
 
 SELECT *
 FROM (
    SELECT
       id,
       name,
       department,
       salary,
       RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dept_rank
    FROM employees
 ) AS ranked_employees
 WHERE dept_rank <= 5;


PARTITION BYの活用

PARTITION BY 句を使用することにより、グループごとに LIMIT のような制限を実現できる。

 -- 各カテゴリの最新3件の商品を取得
 
 SELECT *
 FROM (
    SELECT
       id,
       name,
       category,
       created_at,
       ROW_NUMBER() OVER (PARTITION BY category ORDER BY created_at DESC) AS row_num
    FROM products
 ) AS numbered_products
 WHERE row_num <= 3;


FETCH FIRST構文の非サポート

SQL標準の FETCH FIRST 構文は、MySQL 8.0でもサポートされていない。

 -- エラー : MySQLではFETCH FIRST構文はサポートされていない
 
 SELECT * FROM users
 ORDER BY created_at DESC
 FETCH FIRST 10 ROWS ONLY;  -- エラー
 
 -- MySQLでは以下を使用する
 SELECT * FROM users
 ORDER BY created_at DESC
 LIMIT 10;



ROW_COUNT関数との関係

ROW_COUNT 関数は、直前のSQL文が影響を与えた行数を返す関数である。

ROW_COUNT関数の動作

ROW_COUNT 関数は、INSERTUPDATEDELETE 文で影響を受けた行数を返す。

 -- DELETE文で削除された行数を確認
 
 DELETE FROM logs
 WHERE created_at < DATE_SUB(NOW(), INTERVAL 90 DAY)
 LIMIT 1000;
 
 SELECT ROW_COUNT();  -- 削除された行数が返される


SELECT文では-1を返す

SELECT 文を実行した場合、ROW_COUNT 関数は -1 を返す。

SELECT 文の結果行数を取得する場合は、FOUND_ROWS 関数を使用する必要があるが、これはMySQL 8.0で非推奨となっている。

 SELECT * FROM users LIMIT 10;
 
 SELECT ROW_COUNT();  -- -1が返される


バッチ処理での活用

ROW_COUNT 関数は、バッチ削除やバッチ更新の処理で活用できる。

 -- バッチ削除の例
 
 DELIMITER $$
 
 CREATE PROCEDURE batch_delete()
 BEGIN
    DECLARE affected_rows INT;
    DECLARE total_deleted INT DEFAULT 0;
 
    REPEAT
       DELETE FROM logs
       WHERE created_at < DATE_SUB(NOW(), INTERVAL 90 DAY)
       LIMIT 1000;
 
       SET affected_rows = ROW_COUNT();
       SET total_deleted = total_deleted + affected_rows;
 
       DO SLEEP(0.1);
    UNTIL affected_rows = 0 END REPEAT;
 
    SELECT total_deleted AS total_deleted_rows;
 END$$
 
 DELIMITER ;


SQL_CALC_FOUND_ROWS、FOUND_ROWS関数の非推奨

MySQL 8.0.17以降、SQL_CALC_FOUND_ROWS クエリ修飾子 と FOUND_ROWS 関数は非推奨となった。

 -- 非推奨 : SQL_CALC_FOUND_ROWSの使用
 
 SELECT SQL_CALC_FOUND_ROWS * FROM users LIMIT 10;
 SELECT FOUND_ROWS();  -- 全体の行数を返す (非推奨)


代わりに、COUNT(*) クエリを別途実行することが推奨される。

 -- 推奨 : COUNT(*)を使用
  
 SELECT COUNT(*) AS total FROM users WHERE status = 'active';
 SELECT * FROM users WHERE status = 'active' LIMIT 10;