SQL Server 2014テーブルの行を更新するカーソルを使用するサードパーティアプリケーションがあります。私はこのコードを変更できないため、INSTEAD OF UPDATEトリガーがあり、傍受できるビューにカーソルをポイントする方式を考案しました。更新を完全に制御します。カーソルが悪であることはよく知っていますが、ソースプログラムを変更できないため、カーソルを使用する必要があります。
UPDATE ... WHERE CURRENT OFステートメントを実行すると、クラスター化インデックスscanではなくseekカーソルが現在ポイントしているレコードを検索します。オプティマイザがシークを実行している可能性があるのに、なぜスキャンを実行しているのか判断できません。これらの3つの変数のいずれかをテストから削除すると、インデックスシークが適切に使用されるため、問題を引き起こしているのは、更新トリガーではなくカーソル+ビュー+の組み合わせだと思います。
テーブルの行数は関係ないことに注意してください。オプティマイザは、実行計画に従って常にスキャンを使用します。テストをさらに簡略化するために、トリガー内のロジックをすべて削除しました。
問題を再現するための簡単なコードを次に示します。
CREATE TABLE [dbo].[Person](
[Id] [int] IDENTITY(1,1) NOT NULL,
[Name] [varchar](40) NULL,
CONSTRAINT [PK_Person_Id] PRIMARY KEY CLUSTERED ( [Id] ASC )
WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF,
ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY])
ON [PRIMARY]
GO
CREATE VIEW [dbo].[vPerson]
AS SELECT Id, Name FROM dbo.Person
GO
CREATE TRIGGER [dbo].[InsteadOfUpdate_vPerson] ON [dbo].[vPerson]
INSTEAD OF UPDATE
AS
BEGIN
SET NOCOUNT ON
END
GO
INSERT INTO Person VALUES('John Doe');
GO
--This will use a seek
UPDATE dbo.vPerson
SET Name = 'Jane Doe'
WHERE Id = 1
GO
--This will not use a seek
DECLARE c CURSOR FOR
SELECT * FROM dbo.vPerson
WHERE Id = 1
FOR UPDATE
OPEN c
FETCH NEXT FROM c
UPDATE dbo.vPerson
SET Name = 'Jane Doe'
WHERE CURRENT OF c
CLOSE c
DEALLOCATE c
GO
試してみることに役立つヘルプや提案があれば、大歓迎です。
これは見落としのようです。
行数に関係なく、WHERE CURRENT OF
を使用して更新が実行され、ターゲットビュー(スキーマにバインドされているかどうかにかかわらず)にT-SQLの代わりにT-SQLトリガーがある場合、オプティマイザーは適用スタイルのインデックス付きループ結合を生成できません、またはその他の考慮事項:
これは、テーブルに20,000行近くある例を示しています(AdventureWorksのPersonテーブルからコピーされます)。
結合述語は、シークを生成するために内側にプッシュされるのではなく、ネストされたループ結合演算子自体に「スタック」します。
コードを変更することはできないため、通常のMicrosoftサポートチャネルからこれをバグとして報告する必要があります。 Connectのバグを報告することもできますが、そのルートを介して迅速な応答や修正を得る可能性ははるかに低くなります。
興味を引くために、APIカーソル位置付け更新(内部で最も類似した操作)を使用して、後の計画を立てることができます。
DECLARE
@cur integer,
@scrollopt integer = 2 | 8192 | 32768 | 131072, -- DYNAMIC | AUTO_FETCH | CHECK_ACCEPTED_TYPES | DYNAMIC_ACCEPTABLE
@ccopt integer = 2 | 32768 | 131072, -- SCROLL_LOCKS | CHECK_ACCEPTED_OPTS | SCROLL_LOCKS_ACCEPTABLE
@rowcount integer = 1;
-- Open the cursor
EXECUTE sys.sp_cursoropen
@cur OUTPUT,
N'
SELECT * FROM dbo.vPerson WHERE Id = 1;
',
@scrollopt OUTPUT,
@ccopt OUTPUT,
@rowcount OUTPUT;
-- Request a positioned update
EXECUTE sys.sp_cursor
@cur,
1, -- UPDATE
1, -- row number in buffer
'dbo.vPerson', -- table (unambiguous in this case)
'Name=''Banana'''; -- new value
-- Close
EXECUTE sys.sp_cursorclose -1;
実行計画は次のとおりです。
Personのインデックスシークに注意してください(述語は「スタック」されていません)。
ソースクエリを変更できないため、これは回避策ではありません。問題を回避するためのヒントや計画を立てる方法はありません。オプティマイザは、特定のケースで期待するシークプランを単にできないだけを生成します。たとえばFORCESEEK
ヒントは、オプティマイザが実行プランを作成できなかったことを示すエラーメッセージを表示するだけです。
これがあなたの要件に合うかどうかはわかりませんが、CLR INSTEAD OF
トリガーは、シークプランを生成します。
完全な再現は here ですが、基本的なCLRトリガーは次のとおりです(VBとは異なり、お気軽にc#に変換してください:
Imports System
Imports System.Data
Imports System.Data.SqlClient
Imports System.Data.SqlTypes
Imports Microsoft.SqlServer.Server
Partial Public Class Triggers
<Microsoft.SqlServer.Server.SqlTrigger(Name:="SqlTrigger1", Target:="dbo.Person", Event:="INSTEAD OF UPDATE")> _
Public Shared Sub SqlTrigger1()
SqlContext.Pipe.Send("Trigger FIRED")
'Do some actual work against the table; need to make sure trigger does not fire again.
Try
Using conn As New SqlConnection("context connection=true")
conn.Open()
Dim sqlComm As New SqlCommand
Dim sqlP As SqlPipe = SqlContext.Pipe()
sqlComm.Connection = conn
sqlComm.CommandText = "UPDATE p SET p.Name = i.name + 'z' FROM dbo.Person p INNER JOIN inserted i ON p.Id = i.Id"
sqlComm.ExecuteNonQuery()
End Using
Catch ex As Exception
End Try
End Sub
End Class
トリガーは、カーソルからの初期更新をインターセプトし、レターのタグを付けます z デモ用です。トリガーはビューではなくベーステーブルに対するものである必要があるため、使用が困難かどうかはわかりません。同じ原則が当てはまると思います。メインアップデートをインターセプトし、それとは異なる何かを行います。
プランエクスプローラーからの結果:
試してみて、役に立ったかどうかをお知らせください。