はじめに
データベース管理者として日々現場を見ていると、SQLの記述一つでシステムの命運が分かれる場面に多々遭遇します。特に、アプリケーションからデータベースへクエリを発行する際、無意識に全件取得を行ってしまうケースは、小規模なデータセットでは問題になりませんが、データが数百万、数千万件と膨れ上がった瞬間に致命的なパフォーマンス劣化を引き起こします。
今回は、SQLにおける「取得するデータの行数の上限を設定する」、いわゆるLIMIT句について、その基本的な役割から、実務で直面するパフォーマンス上の落とし穴、そして最適化のための考え方までを深く掘り下げて解説します。
LIMIT句の基本的な役割と構文
LIMIT句は、クエリの結果セットに対して、最大何行までを取得するかを制限するための機能です。多くのRDBMSでサポートされていますが、文法には若干の差異があります。
MySQLやPostgreSQLでは、以下のように記述するのが一般的です。
SELECT FROM orders ORDER BY created_at DESC LIMIT 10;
このクエリは、「注文テーブルから最新の10件を取得する」という、実務で最も頻繁に使用されるパターンのひとつです。
一方で、SQL ServerやOracleでは、LIMIT句そのものではなく、TOP句やFETCH FIRST句を使用します。
— SQL Serverの例
SELECT TOP 10 FROM orders ORDER BY created_at DESC;
— 標準SQL (Oracle 12c以降など)
SELECT FROM orders ORDER BY created_at DESC FETCH FIRST 10 ROWS ONLY;
このように、構文は違えど「結果セットを絞り込む」という目的は共通しています。ここまでは基礎中の基礎ですが、実務で重要になるのは「このLIMIT句がデータベース内部でどのように処理されているか」という点です。
LIMIT句と実行計画:なぜ「速い」のか
DBAが常に意識しているのは、クエリが実行計画においてどのようなコストを払っているかです。LIMIT句がある場合、データベースエンジンは「条件に合致する全件を検索する必要がない」と判断します。
例えば、インデックスが貼られたカラムでソートを行い、LIMIT 10を指定した場合、エンジンはインデックスの先頭から10件分を読み込んだ時点で、検索処理を打ち切ることができます。これを「ショートサーキット(短絡)」と呼びます。
しかし、注意が必要です。LIMIT句をつけたからといって、必ずしも高速化するわけではありません。特にORDER BY句と組み合わせた場合、ソート操作そのものに時間がかかるケースがあります。
実務における「LIMIT」の落とし穴:オフセット問題
LIMIT句とセットでよく使われるのが、OFFSET句です。これは「指定した行数分をスキップする」機能で、Webサイトのページネーション(1ページ目、2ページ目…)で多用されます。
SELECT FROM logs ORDER BY id LIMIT 20 OFFSET 100000;
このクエリを見たとき、多くのエンジニアは「10万件をスキップして20件取るだけなら一瞬だろう」と考えがちです。しかし、データベースの内部では、10万件を一度走査し、それを読み捨てた上で、その後の20件を結果として返しています。
OFFSETの値が大きくなればなるほど、この「読み捨て」のコストが積み重なり、クエリの応答時間は線形に悪化します。これが、大規模テーブルにおけるページネーションのパフォーマンス劣化の主因です。
この問題を回避するための実務的なテクニックが「キーセット・ページネーション(カーソルベース・ページネーション)」です。
— オフセットを使わないページネーション
SELECT FROM logs WHERE id > 100000 ORDER BY id ASC LIMIT 20;
このように、前回の結果の最後のIDを基準にして次を取得する方法に切り替えるだけで、インデックスを活用した高速なアクセスが可能になります。実務では、この手法への移行を強く推奨します。
LIMIT句が効かないケース:結合(JOIN)の罠
次に注意すべきは、JOINを伴うクエリでのLIMIT句です。
SELECT u.name, o.order_date
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
ORDER BY o.order_date DESC
LIMIT 10;
この場合、データベースは「結合後の結果セット」に対してLIMITを適用します。もしusersテーブルとordersテーブルの結合結果が膨大になる場合、結合そのものに多大な負荷がかかります。LIMIT句はあくまで「最終的な結果の行数」を制限するものであり、「結合の計算量」を減らすわけではない、という点に注意してください。
もし、「最新の注文をしたユーザー10名」を取得したいのであれば、先に注文テーブルから10件のIDを絞り込み、その後にユーザーテーブルを結合する、というアプローチが有効です。
— サブクエリで先に絞り込む
SELECT u.name, sub.order_date
FROM (
SELECT user_id, order_date
FROM orders
ORDER BY order_date DESC
LIMIT 10
) sub
JOIN users u ON u.id = sub.user_id;
このような書き換えを行うだけで、結合対象の行数が劇的に減り、パフォーマンスが劇的に向上することがあります。
DBAが教える「LIMIT設計」の心得
実務において、LIMIT句を適切に扱うためのチェックリストを以下にまとめました。
1. インデックスの活用を最優先する
LIMIT句を使用する際は、ORDER BYで指定するカラムに適切なインデックスが貼られているかを確認してください。インデックスがない場合、データベースは「ファイルソート」というメモリを大量に消費する処理を行い、パフォーマンスを著しく低下させます。
2. OFFSETの過信を捨てる
前述の通り、OFFSETは大規模データではパフォーマンスの敵です。可能な限りカーソルベースのページネーションへの移行を検討してください。
3. 「全件取得」の禁止
アプリケーションのコードレビューにおいて、「SELECT 」にLIMITがついていないクエリを発見したら、即座に修正を依頼すべきです。開発環境では問題なくても、本番環境で数百万件のレコードが返され、アプリケーションサーバーのメモリが溢れる(OOM:Out of Memory)事故は後を絶ちません。
4. 実行計画を必ず確認する
EXPLAINコマンドを使用して、LIMIT句がどのように実行されているかを確認する習慣をつけましょう。特に「Using filesort」や「Using temporary」という言葉が出てきたら、インデックスの設計やクエリの見直しのサインです。
まとめ:LIMITは単なる制限ではない
LIMIT句は、単に「表示行数を減らすための道具」ではありません。データベースエンジンに対して「これ以上の検索は不要である」という意図を明確に伝え、計算コストを最小化するための強力な最適化の武器です。
しかし、その使い道を誤れば、逆にシステムのボトルネックを隠蔽し、将来的な障害の種を撒くことにもなりかねません。特に、OFFSETの多用や、結合順序を無視したクエリ記述は、データ量が増加した瞬間にアプリケーションを停止させる要因となります。
私たちDBAは、データが将来的にどう増えるかを予測し、LIMIT句が正しくインデックスの恩恵を受けられるようなクエリ設計を支援する必要があります。クエリ一つ、LIMIT一つにこだわること。それが、堅牢で持続可能なシステムを構築するための第一歩です。
皆さんの現場のクエリも、ぜひ一度、LIMIT句の挙動をEXPLAINで確認してみてください。そこに、さらなるパフォーマンス向上のヒントが眠っているはずです。

コメント