SQL Server の TRY…CATCH が利かずにバッチが停止する――とくに NEXT VALUE FOR でシーケンス名を誤記したときに起きる現象。これはバグではなく、遅延名前解決とコンパイル時エラーの扱いに起因する“仕様”です。本記事では再現手順から原因の深掘り、現場で実践できる回避策、堅牢な実装テンプレートまでを一気に整理します。
問題の核心:なぜ TRY…CATCH に入らずに止まるのか
同じ TRY…CATCH ブロック内でも、シーケンス名の誤記は捕捉できず、0 で割るエラーは捕捉できます。違いは「エラーが発生するタイミング」です。
- シーケンス名の誤記:最適化・実行計画生成の段階で「オブジェクトが存在しない」ことが検出されるコンパイル時エラー(名前解決エラー)。同一スコープの
TRY…CATCHでは捕捉できません。 - 0 除算:実際に式を評価した瞬間に起こる実行時エラー。
TRY…CATCHで捕捉できます。
| ケース | 発生タイミング | 代表的メッセージ | TRY…CATCH での扱い | 備考 |
|---|---|---|---|---|
| シーケンス名の誤記 | コンパイル時(名前解決) | 対象のシーケンスが存在しない、または権限がない | 同一スコープでは捕捉不可 | 遅延名前解決の影響。外側スコープなら捕捉可 |
| 0 除算 | 実行時 | Divide by zero error encountered | 捕捉可能 | エラー番号・重大度は実行時の文脈による |
まずは再現:最小コードで挙動を確認
以下のスクリプトは、同じスコープでは捕捉できないこと、外側スコープなら捕捉できることを示します。
-- 準備:正しいシーケンス(動作確認用)
IF OBJECT_ID(N'dbo.Seq_Demo', 'SO') IS NOT NULL
DROP SEQUENCE dbo.Seq_Demo;
GO
CREATE SEQUENCE dbo.Seq_Demo AS bigint START WITH 1 INCREMENT BY 1;
GO
-- 同一スコープでの TRY…CATCH:誤記は捕捉できない
BEGIN TRY
SELECT NEXT VALUE FOR dbo.Seq_Typo; -- 存在しない
SELECT 1/0; -- ここに到達すれば 0 除算は捕捉可能
END TRY
BEGIN CATCH
PRINT N'CATCH に入りました(同一スコープ)';
PRINT ERROR_MESSAGE();
END CATCH;
GO
-- 内部 SP で誤記 → 外側 SP で捕捉
IF OBJECT_ID(N'dbo.inner_sp', 'P') IS NOT NULL DROP PROCEDURE dbo.inner_sp;
IF OBJECT_ID(N'dbo.outer_sp', 'P') IS NOT NULL DROP PROCEDURE dbo.outer_sp;
GO
CREATE PROCEDURE dbo.inner_sp
AS
BEGIN
BEGIN TRY
SELECT NEXT VALUE FOR dbo.Seq_Typo; -- 誤記
END TRY
BEGIN CATCH
-- ここには来ない
PRINT N'inner_sp の CATCH(到達しない)';
END CATCH
END
GO
CREATE PROCEDURE dbo.outer_sp
AS
BEGIN
BEGIN TRY
EXEC dbo.inner_sp;
END TRY
BEGIN CATCH
PRINT N'外側で捕捉:' + ERROR_MESSAGE();
END CATCH
END
GO
EXEC dbo.outer_sp;
技術的背景:遅延名前解決(deferred name resolution)
SQL Server はストアドプロシージャやバッチを即時に完全解決しない設計を採用しており、テーブル・ビュー・シーケンスなどの存在有無は、実際に当該ステートメントが最適化・実行される段階でチェックされます。これが遅延名前解決です。
このとき発生する「オブジェクトが見つからない」類のエラーはコンパイル時エラーとして扱われ、同一スコープの TRY…CATCH では捕捉できません。一方で、外側のスコープに対してはエラーが伝播するため、呼び出し元に TRY…CATCH を置けば捕捉できます。
現場で使える解決策
捕捉したい/落ちずに制御を取り戻したいケースに対して、現実解を優先順に整理します。
外側スコープでラップする(最小の実装負荷)
モジュールを分け、呼び出し側に TRY…CATCH を置きます。上の inner_sp / outer_sp の例が典型です。
- 長所:既存ロジックを変えずに適用できる。構造化しやすい。
- 短所:すべての呼び出し点でのラップを徹底する必要がある。
事前に存在を検査して明示的に例外化する
呼び出し直前にカタログから存在をチェックし、みずから THROW または RAISERROR で例外化します。THROW のほうが新しく推奨です。
DECLARE @schema sysname = N'dbo', @seq sysname = N'sqc_LogReplic_tmp';
IF OBJECT_ID(QUOTENAME(@schema) + N'.' + QUOTENAME(@seq), 'SO') IS NULL
THROW 50000, N'シーケンスが存在しません: ' + @schema + N'.' + @seq, 1;
-- 以降は安全に参照できる
SELECT NEXT VALUE FOR dbo.sqc_LogReplic_tmp;
この方法は開発時の見落とし、デプロイ漏れ、スキーマ名の勘違いを早期に検知できます。ただし存在確認は「権限の有無」までは保証しません。存在はするが権限がないケースでは、最終的に実行時に権限エラーが発生します(この点は外側スコープで捕捉して復旧してください)。
動的 SQL(sp_executesql)で名前解決を分離する
シーケンス名の解決を別バッチに逃がすと、外側の TRY…CATCH で捕捉しやすくなります。読みやすさとセキュリティ(SQL インジェクション対策)を両立するため、QUOTENAME で厳格に限定します。
CREATE OR ALTER PROCEDURE dbo.TryGetNextSeqValue
@schema sysname,
@seq sysname,
@value sql_variant OUTPUT
AS
BEGIN
SET NOCOUNT ON;
-- 事前検査(任意だが推奨)
IF OBJECT_ID(QUOTENAME(@schema) + N'.' + QUOTENAME(@seq), 'SO') IS NULL
THROW 50001, N'シーケンスが存在しません: ' + @schema + N'.' + @seq, 1;
DECLARE @sql nvarchar(4000) =
N'SELECT @v = NEXT VALUE FOR ' + QUOTENAME(@schema) + N'.' + QUOTENAME(@seq) + N';';
BEGIN TRY
EXEC sp_executesql @sql, N'@v sql_variant OUTPUT', @v = @value OUTPUT;
END TRY
BEGIN CATCH
-- ここで捕捉できる/しやすい
DECLARE @msg nvarchar(4000) =
N'TryGetNextSeqValue 失敗: ' + ERROR_MESSAGE();
THROW 50002, @msg, 1;
END CATCH
END
GO
-- 呼び出し側
DECLARE @v sql_variant;
BEGIN TRY
EXEC dbo.TryGetNextSeqValue @schema = N'dbo', @seq = N'Seq_Typo', @value = @v OUTPUT;
SELECT @v AS NextValue;
END TRY
BEGIN CATCH
PRINT N'呼び出し側で捕捉:' + ERROR_MESSAGE();
END CATCH;
動的 SQL は最小限にとどめ、QUOTENAME を徹底し、呼び出し側でも例外を握りつぶさないことが重要です。
アプリケーション層での防御(推奨)
DB 層での回避に加え、アプリケーション層でも例外を捕捉し、必要ならリトライ・通知・フォールバック(代替の採番方式など)を実装します。ADO.NET / EF / JDBC いずれでも、オブジェクト未存在や権限不足を示すエラーを例外クラスとメッセージで判別できます。
CI/CD での静的解析(未解決参照の検出)
デプロイ前に「未解決の参照」を洗い出します。プロシージャやスクリプトの依存関係は sys.sql_expression_dependencies から抽出できます。referenced_id IS NULL は未解決参照のサインです(すべてのケースを網羅できるわけではありませんが、早期警告として有効)。
SELECT
OBJECT_SCHEMA_NAME(d.referencing_id) AS referencing_schema,
OBJECT_NAME(d.referencing_id) AS referencing_object,
d.referenced_schema_name,
d.referenced_entity_name,
d.is_ambiguous
FROM sys.sql_expression_dependencies AS d
WHERE d.referenced_id IS NULL
ORDER BY referencing_schema, referencing_object;
実戦テンプレート:トランザクション + ロギング + リカバリ
コンパイル時/実行時の双方に備えた“型”を示します。TRY…CATCH 内外の責任分担、XACT_STATE() によるロールバック判断、THROW の再送出を標準化します。
CREATE OR ALTER PROCEDURE dbo.Process_With_Sequence
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON; -- ステートメント失敗でトランザクションを確実に中断
BEGIN TRAN;
BEGIN TRY
DECLARE @next sql_variant;
-- 事前検査 + ベストエフォートの取得
IF OBJECT_ID(N'[dbo].[Seq_Demo]', 'SO') IS NULL
THROW 50010, N'必須のシーケンスが存在しません: dbo.Seq_Demo', 1;
EXEC dbo.TryGetNextSeqValue @schema = N'dbo', @seq = N'Seq_Demo', @value = @next OUTPUT;
-- ここから業務処理
INSERT INTO dbo.SomeTable(KeyCol, ...)
VALUES (CONVERT(bigint, @next), ...);
COMMIT;
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0 ROLLBACK;
DECLARE
@n int = ERROR_NUMBER(),
@s int = ERROR_SEVERITY(),
@st int = ERROR_STATE(),
@p nvarchar(128) = ISNULL(ERROR_PROCEDURE(), N'(ad-hoc)'),
@l int = ERROR_LINE(),
@m nvarchar(2048) = ERROR_MESSAGE();
-- 監査テーブルなどにログ(例)
INSERT INTO dbo.ErrorLog(OccuredAt, Number, Severity, State, ProcedureName, Line, Message)
VALUES (SYSDATETIMEOFFSET(), @n, @s, @st, @p, @l, @m);
-- アプリ層に明確なメッセージで再送出
THROW 50100, N'Process_With_Sequence 失敗: ' + @m, 1;
END CATCH
END
GO
よくある落とし穴と対策
- スキーマ未指定:
NEXT VALUE FORは必ず二部名(schema.object)で書きます。デフォルトスキーマの違いで誤参照・未参照が起こりやすい。 - 権限不足と未存在の区別:どちらも似たエラーメッセージになりがち。カタログで存在チェックしても、最終的な実行は権限で失敗し得ます。外側スコープの
TRY…CATCHと監査ログで診断可能に。 - テスト環境との差分:シーケンスの作成忘れ・スキーマ名の差異が多発。
CREATE SEQUENCE IF NOT EXISTSがないため、デプロイスクリプトに冪等な作成分岐を入れておくと安全です。 - キャッシュ設定:
CACHEを有効にすると性能は向上しますが、サービス停止時に欠番が生じ得ます。業務要件に応じてNOCACHEかアプリ側の期待値を調整。 - リコンパイル:
OPTION (RECOMPILE)は最適化再実行を促しますが、名前解決の性質は変わりません。未存在は未存在のままです。 - 関数・ビューでの使用:
NEXT VALUE FORの使用可否には制約があります。可能でも非決定的であるため、安易に組み込まず専用の取得ルーチンで一元化するのが無難です。
“捕捉できないものはある”を前提に設計する
SQL Server のエラーハンドリングは歴史的経緯から一貫しない点が残っています。すべてのエラーを同一スコープで拾うのは不可能、という前提で設計してください。以下の多層防御をおすすめします。
- モジュール分割 + 外側
TRY…CATCH:コンパイル系エラーは上位に伝播させる。 - 事前検査:
OBJECT_ID(..., 'SO')またはsys.sequencesで存在をチェック。 - 動的 SQL の最小利用:必要時のみ。
QUOTENAME徹底。 - アプリ層の例外処理:ユーザー向けメッセージ、監査、アラート。
- CI の静的解析:未解決参照を検出してビルドを失敗させる。
解決策の選定表
| 解決策 | 効果 | 実装コスト | 副作用・注意点 | 推奨度 |
|---|---|---|---|---|
| 外側スコープでラップ | 高い(誤記も捕捉可能) | 低 | 呼び出し点の統一が必要 | ◎ |
| 事前存在チェック | 高い(早期検知) | 低 | 権限不足は別途対処 | ◎ |
動的 SQL + sp_executesql | 中(ケースにより有効) | 中 | 保守性・セキュリティ配慮必須 | ◯ |
| アプリ層で吸収 | 高い(UI/UX を守れる) | 中 | DB ロジックとの役割分担が鍵 | ◎ |
| CI の静的解析 | 中(早期に芽を摘む) | 中 | 完全ではないが強力 | ◯ |
スキーマをまたぐ・複数 DB をまたぐ場合の指針
- 常に完全修飾名:
Database.Schema.Objectまで書く運用に統一すると、接続先の既定 DB や既定スキーマに依存しません。 - 権限モデル:
EXECUTE AS OWNERやモジュール サイニングで、呼び出し元に権限を委譲する設計も検討。 - 依存関係の可視化:シーケンスは
sys.objects(type=SO)にも出るため、参照関係の棚卸しに活用できます。
テスト戦略:壊しながら確認する
運用前に以下の観点でテストすると安心です。
- シーケンス名を誤記させるテスト(CATCH できること/できないことの境界を確認)。
- シーケンスのスキーマだけを変えるテスト(
dbo以外)。 - 権限を剥がしたユーザーでの実行(存在はするが使えない)。
- トランザクション中に取得 → 例外 → ロールバックの整合性確認(
XACT_STATE())。 - 同時実行(大量の
NEXT VALUE FOR)での性能と欠番の扱い。
ミニ FAQ
Q. 事前に存在チェックしても、本番で CATCH に入らずに止まることがあるのはなぜ?
A. チェックから実行までのわずかな間に、オブジェクトが削除・リネーム・権限変更される“TOCTOU”問題がありえます。完全には防げないため、外側スコープでの捕捉とリカバリを併用してください。
Q. バージョン依存はある?
A. シーケンスが導入された SQL Server 2012 以降で同様の傾向です。互換性レベルを変えても、名前解決とコンパイル時エラーの扱いという根本は同じです。
Q. RAISERROR と THROW のどちらを使うべき?
A. 新規開発は THROW を推奨します。RAISERROR は互換のために残っていますが、THROW の方がスタックや番号の再送出などで扱いやすいです。
チェックリスト:採番に NEXT VALUE FOR を使う前に
- 二部名(できれば三部名)での完全修飾を徹底しているか。
- モジュールを分割し、外側スコープに
TRY…CATCHを置いているか。 - 事前存在チェックと、わかりやすい
THROWメッセージを実装しているか。 - 例外をアプリ層で捕捉し、ユーザー/オペレーターに伝える仕組みがあるか。
- CI で未解決参照(
referenced_id IS NULL)を検出しているか。 - ロギング(
ERROR_*関数)とアラート運用が整っているか。
まとめ
- シーケンス名誤記などの「オブジェクト未存在」系は、同一スコープの
TRY…CATCHでは捕捉できない――遅延名前解決とコンパイル時エラーの性質による仕様です。 - 捕捉したい/サービスを落としたくないなら、外側スコープでラップし、事前存在チェックを入れ、必要に応じて動的 SQLで名前解決を分離します。
- トランザクション制御(
XACT_ABORT・XACT_STATE())とアプリ層の例外処理、CI の静的解析を組み合わせた多層防御が実運用の鍵です。 - divide-by-zero などの通常の実行時エラーは
TRY…CATCHで捕捉可能。性質の異なるエラーを混同しない設計が、障害時の“止まり方”を決めます。

コメント