SQL Serverで大量のIDを IN (...) に並べるとき、「何件まで指定できるのか」「Oracleのような1000件制限はあるのか」「大量に渡すと遅いのか」が気になります。
結論からいうと、SQL Serverの IN 句にはOracleのORA-01795のような「リストは1000件まで」という固定制限はありません。ただし、大量の値を直接並べると、SQL文が長くなり、コンパイル負荷、実行計画、パラメータ数、ネットワーク転送量が問題になります。
SQL Serverの
IN 句は、数件から数十件程度の固定値ならそのままで問題ありません。数百件以上をアプリから毎回渡すなら、VALUES 派生テーブル、#temp 一時テーブル、通常テーブル、テーブル値パラメータを検討します。Microsoft公式ドキュメントでも、非常に大量の値を IN に直接書くと 8623 や 8632 のエラーになることがあり、値をテーブルに格納してサブクエリで使う回避が示されています。判断早見表
まずは、件数と渡し方で次のように判断します。SQL Serverでは「1000件を超えたら必ずエラー」というより、大量の値をどう渡すか、実行計画をどう安定させるかが重要です。
| 状況 | おすすめ | 避けたいこと |
|---|---|---|
| 固定値が数件から数十件 | IN でそのまま書く |
過剰に一時テーブル化する |
| SQL内に小さなID表を作りたい | VALUES 派生テーブル |
巨大な IN 文字列を作る |
| アプリから数百から数千IDを渡す | TVPまたは一時テーブル | @p1,@p2... を大量生成する |
| 繰り返し使う検索条件 | 通常テーブルに条件セットとして保存 | 毎回長いSQLを再生成する |
| 除外条件 | NOT EXISTS |
NULL 混在の NOT IN |
迷ったら、少量は IN、大量・高頻度はテーブル化してJOIN、と考えると大きく外しにくいです。
SQL ServerのIN句に1000件制限はあるか
SQL Serverの IN 句には、Oracleのような1000件固定制限はありません。次のような IN 自体は、1000件を超えたという理由だけで即エラーになるわけではありません。
SELECT * FROM dbo.Orders WHERE order_id IN (1, 2, 3, /* ... */, 1000, 1001, 1002);
ただし、これは「大量に並べても常に安全」という意味ではありません。SQL Serverの公式ドキュメントでは、非常に大量の値を IN の括弧内に明示すると、内部リソースを消費し、エラー 8623 や 8632 が返ることがあると説明されています。回避策としては、値の一覧をテーブルに入れて IN (SELECT ...) にする方法が示されています。
| RDBMS | IN句の代表的な論点 | 考え方 |
|---|---|---|
| Oracle | リストに指定できる式は1000件まで | ORA-01795対策が必要 |
| MySQL | 1000件固定制限はないがSQL長・packet・実行計画が問題 | 大量IDは一時テーブルやJOINを検討 |
| SQL Server | 1000件固定制限ではなく、リソース・パラメータ数・実行計画が問題 | 大量指定はテーブル化してJOINする |
Oracleの1000件制限は Oracle ORA-01795の記事、MySQLのIN句上限は MySQLのIN句上限の記事 で整理しています。SQL Serverでは、同じ「IN句上限」という検索語でも、見るべきポイントが少し違います。
IN句の基本形
IN は、指定した値がリストまたはサブクエリの結果に含まれるかを判定します。少数の固定値であれば、OR を何度も書くより読みやすいです。
-- 固定値のリスト
SELECT *
FROM dbo.Orders
WHERE status IN ('new', 'paid', 'shipped');
-- サブクエリ
SELECT *
FROM dbo.Orders
WHERE customer_id IN (
SELECT customer_id
FROM dbo.PriorityCustomers
);
値が少ない場合はこの書き方で十分です。問題になりやすいのは、アプリケーション側で大量のIDを文字列連結し、IN (1,2,3,...) や IN (@p1,@p2,@p3,...) を毎回生成するケースです。
大量INが遅くなる理由
大量の IN が遅くなる原因は、単に「件数が多いから」だけではありません。SQL文の解析、最適化、実行計画、通信量、型変換などが重なります。
| 原因 | 起きること | 対策 |
|---|---|---|
| SQL文が長い | 送信量・ログ・解析コストが増える | ID一覧をテーブル化する |
| パラメータが多い | アプリ側・DB側の管理が重くなる | テーブル値パラメータや一時テーブルにする |
| 実行計画が複雑 | コンパイル時間が伸びる、8623/8632の可能性 | JOINやサブクエリへ切り替える |
| 型がずれる | 暗黙変換でインデックスを使いにくい | 列と同じ型で渡す |
| 毎回リストが変わる | 実行計画の再利用が効きにくい | 固定構造のテーブル入力にする |
特に order_id が数値列なのに、アプリ側で文字列として IN ('1','2','3') を作ると、暗黙変換や実行計画の問題につながることがあります。インデックスが使われない原因は SQL Serverでインデックスが使われない原因 でも詳しく整理しています。
何件までならIN句でよいか
明確に「何件までなら必ず安全」という境界はありません。テーブルの件数、インデックス、統計情報、SQLの頻度、アプリからの渡し方で変わります。実務上は、次のように考えると判断しやすいです。
| 件数の目安 | 判断 | 理由 |
|---|---|---|
| 数件から数十件 | IN でよい |
SQLが短く、読みやすい |
| 数百件 | 頻度次第で検証 | 実行計画とコンパイル時間を確認したい |
| 数千件 | テーブル化を検討 | SQL長・パラメータ数・最適化コストが目立ちやすい |
| 毎回大量・高頻度 | 一時テーブル、通常テーブル、TVPへ寄せる | セットとしてJOINするほうが安定しやすい |
SQL Serverの最大容量仕様では、ストアドプロシージャやユーザー定義関数のパラメータ数は2100 とされています。アプリで IN (@p1, @p2, ...) のように大量のパラメータを生成する設計は、この上限や管理コストを意識する必要があります。
-- 避けたい例: アプリ側で大量のパラメータを生成する SELECT * FROM dbo.Orders WHERE order_id IN (@p1, @p2, @p3, /* ... */, @p2099, @p2100); -- パラメータ数が増え続ける設計ではなく、ID一覧をテーブルとして渡す設計に寄せる
パラメータ数だけでなく、SQL文の生成、ログ、監視、キャッシュ、アプリ側の実装も複雑になります。大量の値を渡す設計そのものを、早めにテーブル入力へ寄せるのが安定します。
少量ならそのままINで書く
固定値が少ないなら、無理に複雑にする必要はありません。IN は読みやすく、意図も明確です。
SELECT
order_id,
customer_id,
status,
total_amount
FROM dbo.Orders
WHERE status IN ('new', 'paid', 'shipped');
列にインデックスがあり、型も一致していて、値の数が少なければ、まずはこの形で十分です。問題が起きる前からすべてを一時テーブル化すると、逆にSQLが読みにくくなることもあります。
VALUES派生テーブルでJOINする
固定のIDリストをSQL内に書きたいが、IN よりテーブルとして扱いたい場合は、VALUES を派生テーブルにしてJOINできます。
SELECT o.*
FROM dbo.Orders AS o
JOIN (VALUES
(101),
(205),
(309)
) AS ids(order_id)
ON ids.order_id = o.order_id;
Microsoftの VALUES 公式ドキュメントでは、INSERT ... VALUES の VALUES 句として使う場合は1000行制限がありますが、派生テーブルとして使う場合は行数制限がないと説明されています。ただし、SQL文に大量の値を直接埋め込むほどSQLは長くなるため、実務では数百件以上なら一時テーブルやTVPも検討します。
一時テーブルに入れてJOINする
大量のIDを扱うなら、ID一覧を #temp 一時テーブルへ入れてJOINする形が扱いやすいです。インデックスを付けられ、複数のSQLから使い回しやすくなります。
CREATE TABLE #TargetIds (
order_id int NOT NULL PRIMARY KEY
);
INSERT INTO #TargetIds (order_id)
VALUES
(101),
(205),
(309);
SELECT o.*
FROM dbo.Orders AS o
JOIN #TargetIds AS ids
ON ids.order_id = o.order_id;
一時テーブルに主キーやインデックスを付けると、SQL ServerがJOINとして扱いやすくなります。一時テーブルの確認は SQL Serverでテーブル一覧を取得するSQL のtempdb.sys.tables の説明も参考になります。
通常テーブルに入れてJOINする
バッチ処理や画面検索で、対象IDリストを後から追跡したい場合は、一時テーブルではなく通常テーブルに検索条件セットとして保存する方法もあります。
CREATE TABLE dbo.SearchTargetIds (
request_id uniqueidentifier NOT NULL,
order_id int NOT NULL,
created_at datetime2 NOT NULL DEFAULT SYSDATETIME(),
CONSTRAINT PK_SearchTargetIds PRIMARY KEY (request_id, order_id)
);
DECLARE @request_id uniqueidentifier = NEWID();
INSERT INTO dbo.SearchTargetIds (request_id, order_id)
VALUES
(@request_id, 101),
(@request_id, 205),
(@request_id, 309);
SELECT o.*
FROM dbo.Orders AS o
JOIN dbo.SearchTargetIds AS ids
ON ids.order_id = o.order_id
WHERE ids.request_id = @request_id;
検索条件を保存する方式は、ジョブ、監査、再実行、非同期処理と相性がよいです。ただし不要データを削除する運用が必要になるため、単発検索なら一時テーブルやTVPのほうが軽いです。
アプリから大量IDを渡すならテーブル値パラメータを検討する
アプリケーションからSQL Serverへ大量のIDを渡す場合、大量のスカラー引数や文字列連結ではなく、テーブル値パラメータ(TVP)を使う選択肢があります。TVPはユーザー定義テーブル型を使い、複数行を1つのパラメータとしてストアドプロシージャなどへ渡せます。
-- 1. 一度だけ型を作成
CREATE TYPE dbo.IntIdList AS TABLE (
id int NOT NULL PRIMARY KEY
);
GO
-- 2. TVPを受け取るプロシージャ
CREATE PROCEDURE dbo.GetOrdersByIds
@Ids dbo.IntIdList READONLY
AS
BEGIN
SELECT o.*
FROM dbo.Orders AS o
JOIN @Ids AS ids
ON ids.id = o.order_id;
END;
GO
Microsoftのドキュメントでは、TVPは複数行データをTransact-SQL文やルーチンへ渡す方法として説明されています。初期投入時のロックを取得しない、ラウンドトリップを減らせる、セットベース処理に参加できるなどの利点があります。一方で、TVP列には統計情報が保持されない、READONLY として渡す必要がある、という制約もあります。
| TVPの特徴 | 意味 | 実務での注意 |
|---|---|---|
| 強く型付けされる | ユーザー定義テーブル型で列定義を固定する | IDの型を対象列とそろえやすい |
READONLY |
プロシージャ内でTVP自体を更新できない | 加工が必要なら一時テーブルへ移す |
| 統計情報がない | TVP列に通常の統計情報は保持されない | 件数が大きく計画が不安定なら一時テーブルも検討 |
| セットベース処理に参加できる | JOINで対象テーブルと結合できる | 大量パラメータより構造が安定する |
-- TVPの件数が多く、統計情報やインデックスを使いたい場合の考え方
CREATE TABLE #Ids (
id int NOT NULL PRIMARY KEY
);
INSERT INTO #Ids (id)
SELECT id
FROM @Ids;
SELECT o.*
FROM dbo.Orders AS o
JOIN #Ids AS ids
ON ids.id = o.order_id;
NOT INとNULLに注意する
IN より危ないのが NOT IN と NULL の組み合わせです。SQL Serverの公式ドキュメントでも、IN や NOT IN で比較する値にNULLが含まれるとUNKNOWN になり、予期しない結果になることがあると注意されています。
-- 期待通りに除外できないことがある例
SELECT *
FROM dbo.Orders
WHERE customer_id NOT IN (
SELECT customer_id
FROM dbo.BlockedCustomers
);
-- NULLを除外する
SELECT *
FROM dbo.Orders
WHERE customer_id NOT IN (
SELECT customer_id
FROM dbo.BlockedCustomers
WHERE customer_id IS NOT NULL
);
-- NOT EXISTSにする方法もある
SELECT o.*
FROM dbo.Orders AS o
WHERE NOT EXISTS (
SELECT 1
FROM dbo.BlockedCustomers AS b
WHERE b.customer_id = o.customer_id
);
NOT IN とNULLの扱いは、件数制限とは別にバグになりやすいポイントです。除外条件を安全に書くなら、NOT EXISTS も検討します。除外条件全般は SQLで指定した値に一致しないデータを抽出する方法 も参考になります。
実務での選び方
| 状況 | おすすめ | 理由 |
|---|---|---|
| 数件の固定値 | IN |
短く読みやすい |
| SQL内で小さなID表を作りたい | VALUES 派生テーブル |
JOINとして扱える |
| 大量IDを一時的に処理したい | #temp + JOIN |
インデックスを付けやすい |
| アプリから大量IDを渡したい | TVP | 多行データを1つのパラメータとして渡せる |
| 検索条件を後から追跡したい | 通常テーブル | 監査・再実行・非同期処理に向く |
| 除外条件 | NOT EXISTS |
NULLの罠を避けやすい |
大量IDの処理で遅い場合は、SQLの書き換えだけでなく、統計情報や実行計画も確認します。SQL Serverの統計情報を更新する方法 もあわせて見ると、遅いSQLの切り分けがしやすくなります。
よくある質問
SQL ServerのIN句は1000件を超えるとエラーになりますか?
Oracleのような1000件固定制限はありません。ただし、非常に大量の値を直接並べると、リソース消費やエラー 8623 / 8632 の原因になります。大量ならテーブル化してJOINするのが安全です。
VALUESは1000行までですか?
INSERT ... VALUES の VALUES 句として使う場合は1000行制限があります。一方、派生テーブルとして FROM (VALUES ...) の形で使う場合は、公式ドキュメント上は行数制限なしとされています。ただしSQL文が巨大になる問題は残ります。
IN句とJOINはどちらが速いですか?
少量なら差が出ないことも多いです。大量IDを扱う場合は、ID一覧をテーブル化してJOINしたほうが、インデックスや実行計画の面で扱いやすくなります。必ず実行計画と実データで確認します。
アプリ側で文字列連結してIN句を作ってもよいですか?
避けるべきです。SQLインジェクション、型不一致、SQL文の巨大化、実行計画の再利用低下につながります。少量ならパラメータ化し、大量ならTVPや一時テーブルを検討します。
まとめ
SQL Serverの IN 句には、Oracleのような1000件固定制限はありません。しかし、大量の値を直接並べると、SQL文の長さ、コンパイル負荷、実行計画、パラメータ数、エラー 8623 / 8632 などが問題になります。
少量の固定値なら IN で十分です。大量IDを扱うなら、VALUES 派生テーブル、#temp 一時テーブル、通常テーブル、テーブル値パラメータを使い、値の一覧をテーブルとして扱う設計に切り替えましょう。
参考
IN (Transact-SQL) – Microsoft Learn
Maximum capacity specifications for SQL Server – Microsoft Learn

