SQL Serverストアドプロシージャで並列実行は可能?同時に2つのEXEC/SELECTを走らせる方法と注意点

SQL Serverのストアドプロシージャで、互いに依存しない処理を「同時に」走らせたい――EXECを2本並列にしたり、SELECT INTO #tempを並列に作って最後にJOINしたり。結論から言うと、同一SP(同一セッション)内では文を別スレッドで並行実行できません。ではどう設計すれば現実的に並列化できるのか、選択肢と落とし穴を整理します。

目次

SQL Serverのストアドプロシージャ内で「同時に2本走らせたい」が難しい理由

まず押さえておきたいのは、ストアドプロシージャ(以下SP)が動く場所です。SPは「接続(=セッション)」の中で実行され、T-SQLの文は基本的に上から順に実行されます。つまり、同じSPの中でEXECやSELECTを2本書いても、それらは直列で処理され、片方が終わるまで次に進みません。

一方で「SQL Serverは並列実行できる」と聞いたことがあるかもしれません。これは主に1本のクエリの内部を複数スレッドで分担して実行する(クエリプランの並列化)という話です。今回やりたい「文を2本同時に走らせる(文レベルの並列化)」とは別物なので、混同しないのが重要です。

並列化の種類「同時」になる単位例SQL Server側の代表的な実現手段注意点
文レベルの並列(やりたいこと)複数のSQL文/ストアド呼び出しEXEC dbo.CUST1とEXEC dbo.CUST2を同時に別セッションに投げる(Agentジョブ/Service Broker/アプリ側で2接続)結果受け渡し・完了待ち・失敗検知の設計が必要
クエリ内部の並列1本のクエリの演算(スキャン/結合/集計など)大規模JOINや集計を複数スレッドで処理クエリプランのParallelism(MAXDOP、サーバ設定など)CPUを使う。並列度が高すぎると逆効果になることも
非同期/遅延実行(設計上の工夫)完了を待たずに先へ進む重い処理をキューに積んで後で回収Service Broker/外部ジョブ基盤「その場でJOINして返す」要件とは相性が悪い場合がある

よくある「並列でやりたい」2パターン

独立した2つのストアドを同時に走らせたい(2つのEXEC)

たとえば、20分かかる処理が2本あり、互いに依存しないなら「同時に走らせて、全体の待ち時間を20分にしたい」と考えるのは自然です。

-- こう書いても同時には動かない(直列)
EXEC dbo.CUST1;  -- 20分
EXEC dbo.CUST2;  -- 20分

この形のままでは、合計40分になりがちです(もちろんサーバ負荷や待機の状況次第で変わります)。

一時テーブルを2本並列に作ってからJOINしたい(2つのSELECT INTO)

次のように#T1と#T2をそれぞれ作り、最後にJOINして最終結果を作りたいケースです。JOINで一発に書けるのは理解しているものの、「SPの中で並列に作れないか」を知りたい、という状況ですね。

-- こう書いても同時には作られない(直列)
SELECT ... INTO #T1 FROM ...;  -- 20分
SELECT ... INTO #T2 FROM ...;  -- 20分

SELECT ...
FROM #T1
JOIN #T2 ON ...;

結論は同じで、SP内だけで#T1と#T2を“別スレッドで同時生成”はできません。並列化するなら、実行単位をセッションごと分ける必要があります。

結論:1つのSP(同一セッション)だけで「2本同時」は基本できない

同一セッション内で同時に複数のSQL文を走らせるためのT-SQL構文(たとえばEXEC ... ASYNCのようなもの)は用意されていません。SPは「処理を順番に書く」ための器で、並列オーケストレーションを担う作りにはなっていない、という整理が現実に近いです。

そのため、並列化したい場合の基本方針は次のどれかになります。

  • SQL Server Agentジョブを使って、別セッション(別実行コンテキスト)で処理を回す
  • Service Brokerを使って、キューに投入しアクティベーションで別セッション実行させる
  • アプリ(クライアント)側で2接続を開き、非同期に2本投げて待ち合わせる

ここからは、それぞれのメリット・デメリットと、設計のツボ(特に結果の受け渡し)を具体的に見ていきます。

方式並列化の実現得意な用途メリットデメリット/注意点
SQL Server Agentジョブ=別セッションで実行バッチ/夜間処理、手順が固定の並列実装が比較的わかりやすい。運用面で監視しやすい権限設計が必要。完了待ち・失敗検知・結果受け渡しを作り込む必要
Service Brokerキュー投入→アクティベーションSPが別セッションで動く非同期処理、イベント駆動、処理量が増減するワークロードスケーラブル。キュー制御・リトライ設計と相性が良い構成・運用が複雑。設計を誤ると「詰まり」や「毒メッセージ」問題が出る
アプリ側で並列クライアントが2接続で同時実行オンライン処理、API、バッチのオーケストレーションDB側を複雑化しにくい。パラメータ渡しが自然アプリ改修が必要。DB内で#一時テーブル共有はできない

解決策A:SQL Server Agentジョブで「別セッション実行」にする

SQL Server Agent(エージェント)が使える環境なら、もっとも手堅いのがこの方法です。ジョブは別セッションとして実行されるため、メインSPから2つのジョブを起動すれば、結果として並列に動きます。

基本形:2ジョブを起動して並列実行させる

ジョブ側にはステップを作り、EXEC dbo.CUST1のような実行コマンドを紐付けます。メインSPからは次のように起動します。

-- メインSPからジョブを起動(起動自体はすぐ戻る)
EXEC msdb.dbo.sp_start_job @job_name = N'Run_CUST1';
EXEC msdb.dbo.sp_start_job @job_name = N'Run_CUST2';

ここで重要なのは、sp_start_jobは「起動要求」を出すだけで、ジョブの完了を待ってくれるわけではない点です。完了待ちや失敗検知をしたいなら、メインSP側でステータスを確認する仕組みを用意します。

実務で困りやすい点:パラメータ渡しと結果の受け渡し

「CUST1とCUST2を並列に動かし、結果を突き合わせて返したい」ケースで必ず出てくるのが、次の2点です。

  • ジョブを起動する際に、実行IDや対象期間などのパラメータをどう渡すか
  • ジョブで作った中間結果を、メインSPがどう回収するか

Agentジョブは“外部からパラメータ付きで呼び出す”ことが得意ではありません。そこで現実的には、メインSPが実行IDを採番し、ステージングテーブルに「指示」と「結果」を持たせる設計が扱いやすいです。

設計例:実行IDで衝突を避けるステージング方式

メインSPが一意な実行ID(GUIDなど)を発行し、ジョブはそのIDに紐づく行だけを処理して結果を書き込みます。これなら複数ユーザーが同時に実行しても衝突しにくく、後片付けも自動化できます。

-- 例:制御テーブル(実行の指示と状態を管理)
CREATE TABLE dbo.ParallelRun (
    RunId           uniqueidentifier NOT NULL PRIMARY KEY,
    RequestedAt     datetime2(0)      NOT NULL DEFAULT SYSUTCDATETIME(),
    Status_CUST1    varchar(20)       NOT NULL DEFAULT 'Queued', -- Queued/Running/Done/Failed
    Status_CUST2    varchar(20)       NOT NULL DEFAULT 'Queued',
    Error_CUST1     nvarchar(4000)    NULL,
    Error_CUST2     nvarchar(4000)    NULL
);

-- 例:中間結果(RunIdで区別)
CREATE TABLE dbo.Stage_T1 (
    RunId uniqueidentifier NOT NULL,
    Key1  int NOT NULL,
    ColA  nvarchar(100) NULL,
    PRIMARY KEY (RunId, Key1)
);

CREATE TABLE dbo.Stage_T2 (
    RunId uniqueidentifier NOT NULL,
    Key1  int NOT NULL,
    ColB  nvarchar(100) NULL,
    PRIMARY KEY (RunId, Key1)
);

ジョブ側のステップは、次のように「まだ処理していないRunIdを1件“掴む”→処理→結果を書き込む」という流れにします。複数同時実行を想定するなら、掴み取りはロック戦略が肝です(UPDLOCKやREADPASTなどで競合を避けます)。

-- ジョブ側(CUST1担当)のイメージ(「未処理のRunIdを1件掴む」→処理)
DECLARE @RunId uniqueidentifier;
DECLARE @Picked TABLE (RunId uniqueidentifier);

;WITH cte AS (
    SELECT TOP (1) RunId
    FROM dbo.ParallelRun WITH (UPDLOCK, READPAST, ROWLOCK)
    WHERE Status_CUST1 = 'Queued'
    ORDER BY RequestedAt
)
UPDATE cte
SET Status_CUST1 = 'Running'
OUTPUT inserted.RunId INTO @Picked;

SELECT TOP (1) @RunId = RunId FROM @Picked;

IF @RunId IS NULL
    RETURN; -- 仕事がない(先に別ワーカーが掴んだ等)

-- ここで実処理を実行し、結果をStage_T1へ書き込む
-- EXEC dbo.CUST1 @RunId = @RunId; など(実装に合わせて)

UPDATE dbo.ParallelRun
SET Status_CUST1 = 'Done'
WHERE RunId = @RunId;

「ジョブを起動する」だけではなく、ジョブが“どの実行の仕事をするのか”をDB内で解決することで、パラメータ渡しの壁を越えやすくなります。

メインSP側:起動→完了待ち→JOIN→後片付け

メインSPはRunIdを発行し、2ジョブを起動し、dbo.ParallelRunのステータスを見て待ち合わせます。待機は無限ループにせず、タイムアウトや失敗時の扱いも決めておくのが安全です。

DECLARE @RunId uniqueidentifier = NEWID();

INSERT INTO dbo.ParallelRun (RunId) VALUES (@RunId);

EXEC msdb.dbo.sp_start_job @job_name = N'Run_CUST1';
EXEC msdb.dbo.sp_start_job @job_name = N'Run_CUST2';

-- 完了待ち(シンプルなポーリング例)
DECLARE @waitCount int = 0;

WHILE 1 = 1
BEGIN
    DECLARE @s1 varchar(20), @s2 varchar(20);

    SELECT
        @s1 = Status_CUST1,
        @s2 = Status_CUST2
    FROM dbo.ParallelRun
    WHERE RunId = @RunId;

    IF (@s1 IN ('Done','Failed') AND @s2 IN ('Done','Failed'))
        BREAK;

    -- 例:最大10分で諦める(要件に合わせて調整)
    IF (@waitCount > 120)
        THROW 50000, 'Parallel jobs timeout.', 1;

    SET @waitCount += 1;
    WAITFOR DELAY '00:00:05';
END;

-- どちらか失敗していたらエラーを返す
IF EXISTS (
    SELECT 1
    FROM dbo.ParallelRun
    WHERE RunId = @RunId
      AND (Status_CUST1 = 'Failed' OR Status_CUST2 = 'Failed')
)
BEGIN
    THROW 50001, 'Parallel job failed.', 1;
END;

-- 成功したらJOINして最終結果
SELECT ...
FROM dbo.Stage_T1 t1
JOIN dbo.Stage_T2 t2
  ON t1.RunId = t2.RunId
 AND t1.Key1  = t2.Key1
WHERE t1.RunId = @RunId;

-- 後片付け(必要に応じて非同期削除でも)
DELETE FROM dbo.Stage_T1 WHERE RunId = @RunId;
DELETE FROM dbo.Stage_T2 WHERE RunId = @RunId;
DELETE FROM dbo.ParallelRun WHERE RunId = @RunId;

この方式のメリットは「確実に別セッションで動く」「運用で見える化しやすい」ことです。逆に、DB内で完結させるにはステージングテーブルと状態管理がほぼ必須になり、そこが設計コストになります。

権限・運用の注意点

  • SQL Server Agentが必要です。エディション/サービス形態によっては利用できない場合があります。
  • ジョブ起動にはmsdbの権限が必要です。運用ルールとして「アプリユーザーにジョブ起動を許可するか」をDBAと握る必要があります。
  • ジョブ側でエラーが起きた場合、メインSPは必ず検知して中断できるようにします(“起動して終わり”にしない)。
  • 並列化によってサーバ資源(CPU/I/O/tempdb)を奪い合い、結果的に遅くなることもあります。後述のチェックポイントも参照してください。

解決策B:Service Brokerで「非同期キュー→別セッション実行」にする

Service Brokerは、DB内にメッセージキューを持ち、メッセージに応じて処理を起動できる仕組みです。キューにメッセージを入れると、アクティベーション(自動起動)されたストアドプロシージャが別セッションで動き、バックグラウンド処理を実現できます。

「メインSPがすぐ返してよい」「結果は後で取りに来る」タイプの要件なら非常に強力です。一方で「メインSPがその場でJOINして結果を返したい」場合は、結局待ち合わせが必要になるため、メリットと複雑さのトレードオフを見極める必要があります。

ざっくり構成イメージ

  • メインSP:RunIdを採番し、タスク(CUST1/CUST2)を表すメッセージをキューに投入
  • ワーカーSP:キューから受信し、該当タスクを実行してステージングテーブルへ結果を書き込み、状態を更新
  • メインSP(同期で返す場合):状態テーブルをポーリングして2タスク完了を待つ

コード例まで踏み込むと長くなりますが、雰囲気としては次のように「メッセージを投げて終わり」「受け取った側が処理する」というモデルになります。

-- メインSP側(概念例):タスクをキューに投入
-- SEND ... MESSAGE TYPE ... (payloadにRunIdやTaskTypeを入れる)

-- ワーカー側(概念例):RECEIVEして処理
-- RECEIVE TOP(1) ... FROM dbo.Queue;
-- IF TaskType='CUST1' THEN EXEC dbo.CUST1 ...

Service Broker採用時に必ず検討したいこと

観点検討ポイントありがちな落とし穴
運用監視キューの滞留、失敗メッセージ、処理遅延の検知方法「気づいたらキューが詰まっていた」
例外処理リトライ方針、毒メッセージ対策、エラーログ同じメッセージで失敗し続けて処理が停止
並列度MAX_QUEUE_READERSやワーカー数の設計並列を上げすぎてCPU/tempdbが飽和
設計の複雑さ会話(conversation)管理、セキュリティ、保守導入しただけで「直せない箱」になりがち

Service Brokerは「並列化の道具」というより、非同期・疎結合の基盤です。単に重い処理を2本同時に走らせたいだけなら、Agentやアプリ側並列の方がコストが低いことが多いです。逆に、将来的にタスクが増える見込みがある、実行タイミングがばらける、負荷を平準化したい、といった要件があるなら長期的に効いてきます。

現実解:アプリ(クライアント)側で2接続を開いて非同期に実行する

DBの中で無理にオーケストレーションを頑張るより、クライアント側で2本投げて待つのが一番シンプル、という結論になることも多いです。SQL Serverにとっては「別セッションが2つ動く」だけなので、仕組みとしても自然です。

たとえばC#なら、2つのSqlConnectionを開いてExecuteNonQueryAsyncなどで同時実行し、両方終わったタイミングで集約クエリを投げます(擬似コード)。

// 擬似コード(概念)
Task t1 = RunProcAsync(conn1, "dbo.CUST1", runId);
Task t2 = RunProcAsync(conn2, "dbo.CUST2", runId);
await Task.WhenAll(t1, t2);

// 両方終わったら、結果テーブルをJOINして取得
var result = await QueryAsync(conn3, "SELECT ... FROM Stage_T1 JOIN Stage_T2 ...");

この場合も、後述の通り#一時テーブルは共有できないため、結果を突き合わせるならステージングテーブル方式が基本になります。ただし、アプリ側ならRunIdの受け渡しが簡単で、エラー処理やタイムアウトもアプリの流儀で扱いやすいのが利点です。

最重要注意:#一時テーブルはセッションをまたげない

並列化の議論でつまずきやすいのが一時テーブルのスコープです。ローカル一時テーブル(#T)は、作成したセッションの中だけで有効です。同じSP内や同じ接続で呼び出した子SPからは参照できても、AgentジョブやService Broker、別接続からは見えません。

つまり、例2のように「#T1と#T2を別セッションで並列生成して、メインSPでJOINしたい」という設計は、そのままでは成立しません。成立させるには、結果を共有できる置き場所に出す必要があります。

中間結果の置き場所共有範囲メリットデメリットおすすめ度
永続ステージングテーブル(RunIdで分離)全セッション確実。監視しやすい。後処理を自動化しやすい設計が必要。掃除(保持期間)を決めないと肥大化高
グローバル一時テーブル(##T)全セッション作るのは簡単。短期の共有に使える名前衝突、残骸、権限、予期せぬ参照のリスク低(基本は避ける)
ローカル一時テーブル(#T)作成したセッションのみtempdbに閉じて安全。速度面で有利なことも別セッションに渡せないため並列化の受け渡しに不向き用途限定

どうしてもグローバル一時テーブルを使う場合は、最低限次の対策を入れてください(それでも推奨は「永続ステージング+RunId」です)。

  • 名前衝突を避けるため、テーブル名に実行IDや@@SPID相当の情報を含める(動的SQLが必要になりがち)
  • TRY...CATCHで必ずDROP TABLEする(失敗時の残骸が最大の事故要因)
  • 掃除漏れに備え、定期クリーンアップ(ジョブ等)を用意する

並列化の前に確認したい:本当に速くなるか

並列化は万能ではありません。特に「どちらも20分かかる」処理を2本同時に走らせると、CPU・I/O・tempdb・メモリなどの資源を奪い合い、2本とも遅くなることがあります。結果として合計時間が短縮されない、むしろ伸びる、というのは珍しくありません。

チェックポイント:ボトルネックを見誤らない

  • CPUが原因:実行中にCPU使用率が高止まりするなら、同時実行でさらに飽和しがちです。各処理の並列度(MAXDOP)を抑える、クエリ改善でCPU負荷を下げる、ハード増強を検討します。
  • I/Oが原因:ストレージ待ちが多いなら、2本同時でI/Oが倍になり、待機が増える可能性があります。索引の追加、不要列の削減、フィルタ条件の見直し、集計の事前計算などでI/Oを減らす方が効くこともあります。
  • tempdbが原因:SELECT INTOや大量ソート/ハッシュ結合はtempdbに負荷をかけます。2本同時でtempdb競合が起きると一気に遅くなります。tempdb構成やクエリ見直しが必要です。
  • ロック/ブロッキング:同じテーブルを更新する処理を並列に走らせると、ブロッキングやデッドロックのリスクが上がります。トランザクション範囲を短くする、更新順序を揃える、分割して更新するなどが有効です。

「並列化しない方が良い」判断になりやすいケース

  • 2本とも同じ巨大テーブルをフルスキャンしている(I/O競合で共倒れしやすい)
  • 2本とも並列クエリでCPUを使い切っている(同時実行でスレッド枯渇や待機増)
  • 2本が同一テーブルを更新する(ロック競合、デッドロック)
  • 「最終的にはJOINする」ので、論理的に1本のクエリにまとめられる(最適化の余地が大きい)

逆に「ストレージ待ちが少なくCPUに余裕がある」「2本が完全に別のデータ領域を読む」「処理が外部I/O待ちで止まる」など、資源が噛み合わない場合は並列化で体感が改善することもあります。闇雲に並列化するより、まず実行計画や待機の傾向を見て“何が詰まっているか”を把握するのが近道です。

まとめ:SP内の「2本同時」は別セッション化が基本。成功の鍵は受け渡し設計

SQL Serverのストアドプロシージャ内で、独立した2つのEXECやSELECTを“同時に”走らせることは、素のT-SQLだけでは実現できません。やるなら別セッションで動かす必要があり、代表的な選択肢はSQL Server Agentジョブ、Service Broker、そしてアプリ側の非同期実行です。

特に一時テーブル(#)はセッションをまたげないため、並列化して最後に突き合わせたい場合は、RunIdで分離したステージングテーブルを使う設計が現実的です。最後にもう一度、判断軸をシンプルにまとめます。

  • バッチ的で運用監視も重視:SQL Server Agent
  • 非同期・疎結合でスケールさせたい:Service Broker
  • DBを複雑にしたくない/アプリで制御できる:クライアント側で2接続並列

「並列にしたい」背景には、たいてい性能課題があります。並列化は選択肢の一つですが、クエリ設計・索引・tempdb・並列度(MAXDOP)といった基本のチューニングで一発解決することも多いので、要件(同期で返すのか、後で回収で良いのか)とボトルネック(CPU/I/O/ロック)をセットで見て、最適な実装を選んでください。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次