ストアド プロシージャを繰り返し実行する次のコードがあります。SQL ステートメントを文字どおりに実行するとうまく機能するので、実行していることをカプセル化したストアド プロシージャを作成しました。
foreach (string worker in workers)
{
_gzClasses.ExecuteCommand("EXEC dbo.Session_Aggregate @workerId = {0}, @timeThresh = {1}", worker, SecondThreshold);
Console.WriteLine("Inserted sessions for {0}", worker);
}
次に、各呼び出しが生成する行数を知りたかったので@@rowcount
、出力パラメーターとして返すように SP を少し変更しました。DataContext を使用して出力パラメーターを使用してコマンドを実行することはできないため、上記の for ループ内のコードを次のように変更する必要がありました。
using (var cn = new SqlConnection(CnStr))
{
cn.Open();
using (var cmd = new SqlCommand("Session_Aggregate",
cn) {CommandTimeout = 300})
{
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.AddWithValue("@workerId", worker);
cmd.Parameters.AddWithValue("@timeThresh", SecondThreshold);
SqlParameter sessions = cmd.Parameters.Add("@sessions", SqlDbType.Int);
sessions.Direction = ParameterDirection.Output;
cmd.ExecuteNonQuery();
Console.WriteLine("Inserted {1} sessions for {0}", worker, sessions.Value);
}
}
これは機能しますが、他のクエリよりもはるかに遅く実行されます。パラメータ盗聴のケースかもしれないと思ったのでCommandType.Text
、文字列に変更して使用しましたEXEC Session_Aggregate ... WITH RECOMPILE
。しかし、その場合、out パラメータ@session
が定義されていないというエラーが表示され続けます。いずれにせよ、SQL コマンドは SSMS で 1 秒未満で実行されますが、クエリはほとんど実行されません。
誰かが何が起こっているのかを理解するのを手伝ったり、物事をスピードアップする方法を見つけたりできる場合に備えて、ストアド プロシージャを次に示します。また、ここで何が起こっているかを適切にプロファイルする方法についての指針も示します。CommandType.StoredProcedure
VSによってSQLに送信される実際のコマンドを見ることさえできません。
PROCEDURE [dbo].[Session_Aggregate]
-- Add the parameters for the stored procedure here
@workerId varchar(64) = 0,
@timeThresh dateTime = '13 July 2007 11:27:46'
@sessions INT OUTPUT
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
-- Insert statements for procedure here
INSERT INTO e_activeSessions
SELECT *
FROM (
SELECT workerId, startTime, COUNT(*) as totalTasks, MAX(timeInSession) as totalTime,
MIN(dwellTime) as minDwell, MAX(dwellTime) as maxDwell, AVG(dwellTime) as avgDwell, STDEV(dwellTime) as stdevDwell,
SUM(CAST(wrong80 as INT)) + SUM(CAST(correct80 as INT)) as total80, SUM(CAST(correct80 as INT)) as correct80,
SUM(CAST(correct80 as FLOAT)) / NULLIF(SUM(CAST(wrong80 as INT)) + SUM(CAST(correct80 as INT)), 0 ) as percent80
FROM (
SELECT *, (SELECT MAX(timeStamp)
FROM workerLog w where dwellTime is null AND timeInSession = 0 AND workerId = @workerId AND w.timeStamp <= workerLog.timeStamp
AND w.timeStamp >= @timeThresh) as startTime
FROM workerLog where workerId = @workerId) t
GROUP BY startTime, workerId) f
WHERE startTime is NOT NULL AND f.totalTasks > 1 AND totalTime > 0;
SET @sessions = @@ROWCOUNT;
END
編集:元のクエリの実行計画に関係なく、一時テーブルを作成することで大幅に高速化されました。クエリを分析することで SQL がこれを行うと思っていましたが、おそらく間違っていました。また、 SQL Server の新しいバージョンでは、実行プランが非常に異なるサイズのデータに対するパラメーター スニッフィングの影響を軽減するヒントを発見しました。OPTIMIZE FOR UNKNOWN
PROCEDURE [dbo].[Session_Aggregate]
-- Add the parameters for the stored procedure here
@workerId varchar(64) = 0,
@timeThresh dateTime = '13 July 2007 11:27:46',
@sessions INT OUTPUT
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
-- Insert statements for procedure here
CREATE TABLE #startTimes
(
startTime DATETIME
);
CREATE INDEX Idx_startTime ON #startTimes(startTime);
INSERT INTO #startTimes
SELECT timeStamp FROM workerLog
WHERE dwellTime is null AND timeInSession = 0
AND workerId = @workerId AND timeStamp >= @timeThresh;
INSERT INTO e_activeSessions
SELECT *
FROM (
SELECT workerId, startTime, COUNT(*) as totalTasks, MAX(timeInSession) as totalTime,
MIN(dwellTime) as minDwell, MAX(dwellTime) as maxDwell, AVG(dwellTime) as avgDwell, STDEV(dwellTime) as stdevDwell,
SUM(CAST(wrong80 as INT)) + SUM(CAST(correct80 as INT)) as total80, SUM(CAST(correct80 as INT)) as correct80,
SUM(CAST(correct80 as FLOAT)) / NULLIF(SUM(CAST(wrong80 as INT)) + SUM(CAST(correct80 as INT)), 0 ) as percent80
FROM (
SELECT *, (SELECT MAX(startTime) FROM #startTimes where startTime <= workerLog.timeStamp) as startTime
FROM workerLog where workerId = @workerId) t
GROUP BY startTime, workerId) f
WHERE startTime is NOT NULL AND f.totalTasks > 1 AND totalTime > 0
OPTION (OPTIMIZE FOR UNKNOWN);
SET @sessions = @@ROWCOUNT;
END;
追加の簡素化: SP を DBML ファイルにドラッグすると、次のことができます。
foreach (string worker in workers)
{
int? rows = 0;
_gzClasses.Session_Aggregate(worker, SecondThreshold, ref rows);
Console.WriteLine("Inserted {1} sessions for {0}", worker, rows);
}