Sample output:
Source code:
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
IF NOT EXISTS (
SELECT *
FROM sys.procedures p
JOIN sys.schemas s ON p.schema_id = s.schema_id
WHERE s.name = 'dbo'
AND p.name = 'sp_DeadlockAnalyzer'
)
BEGIN
EXEC ('CREATE PROCEDURE dbo.sp_DeadlockAnalyzer AS SELECT 1');
END
GO
ALTER PROCEDURE dbo.sp_DeadlockAnalyzer
(
@xdl XML – Deadlock graph event
)
AS
BEGIN
IF OBJECT_ID(N'tempdb.dbo.#Resources') IS NOT NULL
DROP TABLE #Resources;
IF OBJECT_ID(N'tempdb.dbo.#Processes') IS NOT NULL
DROP TABLE #Processes;
– Information about resources
CREATE TABLE #Resources (
Id INT IDENTITY PRIMARY KEY,
Resource_LockGranularity SYSNAME NOT NULL,
Resource_DatabaseId INT NOT NULL,
Resource_HoBtID BIGINT NOT NULL,
Resource_ObjectName SYSNAME NULL,
Resource_IndexName SYSNAME NULL,
ProcessOwner_Id SYSNAME NOT NULL,
ProcessOwner_LockMode SYSNAME NOT NULL,
ProcessWaiter_Id SYSNAME NOT NULL,
ProcessWaiter_LockMode SYSNAME NOT NULL
);
INSERT #Resources (
Resource_LockGranularity ,
Resource_DatabaseId ,
Resource_HoBtID ,
Resource_ObjectName ,
Resource_IndexName ,
ProcessOwner_Id ,
ProcessOwner_LockMode ,
ProcessWaiter_Id ,
ProcessWaiter_LockMode
)
SELECT Resource_LockGranularity= x.XmlCol.value('local-name(.)','SYSNAME'),
Resource_Database = x.XmlCol.value('@dbid','INT'),
Resource_HoBtID = x.XmlCol.value('@hobtid','BIGINT'),
Resource_ObjectName = QUOTENAME(PARSENAME(x.XmlCol.value('@objectname','SYSNAME'), 3)) + '.' + QUOTENAME(PARSENAME(x.XmlCol.value('@objectname','SYSNAME'), 2)) + '.' + QUOTENAME(PARSENAME(x.XmlCol.value('@objectname','SYSNAME'), 1)),
Resource_IndexName = QUOTENAME(x.XmlCol.value('@indexname','SYSNAME')),
ProcessOwner_Id = own.XmlCol.value('@id', 'SYSNAME'),
ProcessOwner_LockMode = own.XmlCol.value('@mode', 'SYSNAME'),
ProcessWaiter_Id = wtr.XmlCol.value('@id', 'SYSNAME'),
ProcessWaiter_Mode = wtr.XmlCol.value('@mode', 'SYSNAME')
FROM @xdl.nodes('deadlock-list/deadlock/resource-list/*') x(XmlCol)
OUTER APPLY x.XmlCol.nodes('owner-list/owner') own(XmlCol)
OUTER APPLY x.XmlCol.nodes('waiter-list/waiter') wtr(XmlCol);
CREATE TABLE #Processes (
SPID INT NOT NULL,
IsVictim BIT NOT NULL,
Database_Name SYSNAME NOT NULL,
InputBuffer XML NULL,
TransactionName SYSNAME NULL,
IsolationLevel SYSNAME NULL,
Snapshot_Isolation_State SYSNAME NULL,
DeadlockPriority SMALLINT NULL,
LogUsed INT NULL,
ClientApp SYSNAME NULL,
HostName SYSNAME NULL,
LoginName SYSNAME NULL,
Database_Id INT NOT NULL,
CallStack XML NULL,
Process_Id SYSNAME PRIMARY KEY
);
INSERT #Processes
SELECT y.SPID, y.IsVictim,
QUOTENAME(DB_NAME(y.Database_Id)) AS Database_Name, y.InputBuffer,
y.TransactionName, y.IsolationLevel, db.snapshot_isolation_state_desc, y.DeadlockPriority, y.LogUsed,
y.ClientApp, y.HostName, y.LoginName, y.Database_Id,
y.CallStack,
y.Process_Id
FROM (
SELECT Process_Id = x.XmlCol.value('(@id)[1]', 'SYSNAME'),
SPID = x.XmlCol.value('(@spid)[1]', 'INT'),
IsVictim = x.XmlCol.exist('(.)[(@id)[1] = (../../@victim)[1]]'),
– CurrentDatabase= DB_NAME(x.XmlCol.value('(@currentdb)[1]', 'SMALLINT')) + N' (' + x.XmlCol.value('(@currentdb)[1]', 'NVARCHAR(5)') + N')',
Database_Id = x.XmlCol.value('(@currentdb)[1]', 'SMALLINT'),
InputBuffer = x.XmlCol.query('inputbuf'),
TransactionName = x.XmlCol.value('(@transactionname)[1]', 'SYSNAME'),
IsolationLevel = x.XmlCol.value('(@isolationlevel)[1]', 'SYSNAME'),
DeadlockPriority = x.XmlCol.value('(@priority)[1]', 'SMALLINT'),
LogUsed = x.XmlCol.value('(@logused)[1]', 'INT'),
ClientApp = x.XmlCol.value('(@clientapp)[1]', 'SYSNAME'),
HostName = x.XmlCol.value('(@hostname)[1]', 'SYSNAME'),
LoginName = x.XmlCol.value('(@loginname)[1]', 'SYSNAME'),
CallStack = x.XmlCol.query('./executionStack')
FROM @xdl.nodes('deadlock-list/deadlock/process-list/process') x(XmlCol)
) y INNER JOIN sys.databases db ON y.Database_Id = db.database_id;
DECLARE @DistinctProcesses TABLE (SPID_Description VARCHAR(16) PRIMARY KEY);
INSERT @DistinctProcesses (SPID_Description)
SELECT DISTINCT QUOTENAME('SPID ' + CONVERT(VARCHAR(11), p.SPID))
FROM #Processes p;
DECLARE @DistinctProcessesList NVARCHAR(4000);
SET @DistinctProcessesList = '';
SELECT @DistinctProcessesList = @DistinctProcessesList + ', ' + dp.SPID_Description
FROM @DistinctProcesses dp;
SET @DistinctProcessesList = STUFF(@DistinctProcessesList, 1, 2, '');
DECLARE @SqlStatement NVARCHAR(MAX);
SET @SqlStatement = N'
SELECT t.Resource_ObjectName, t.Resource_IndexName, t.Resource_LockGranularity,
' + @DistinctProcessesList + ',
t.Resource_DatabaseId, t.Resource_HoBtID
FROM (
SELECT x.Resource_LockGranularity, x.Resource_ObjectName, x.Resource_IndexName,
''SPID '' + CONVERT(VARCHAR(11), y.SPID) AS SPID_Description, y.LockInfo,
x.Resource_DatabaseId, x.Resource_HoBtID
FROM (
SELECT r.Resource_LockGranularity, r.Resource_ObjectName, r.Resource_IndexName,
r.ProcessOwner_Id, ''acqr '' + r.ProcessOwner_LockMode AS OwnerLockInfo,
r.ProcessWaiter_Id, ''wait '' + r.ProcessWaiter_LockMode AS WaitwerLockInfo,
r.Resource_DatabaseId, r.Resource_HoBtID
FROM #Resources r
) x
CROSS APPLY (
SELECT p.SPID, x.OwnerLockInfo
FROM #Processes p WHERE p.Process_Id = x.ProcessOwner_Id
UNION ALL
SELECT p.SPID, x.WaitwerLockInfo
FROM #Processes p WHERE p.Process_Id = x.ProcessWaiter_Id
) y(SPID, LockInfo)
) z
PIVOT( MAX(z.LockInfo) FOR z.SPID_Description IN (' + @DistinctProcessesList + ') ) t';
EXEC (@SqlStatement);
– Information about server processes / conections including SQL batches
SELECT * FROM #Processes ORDER BY IsVictim DESC;
– Statements and execution plans
DECLARE @Statements TABLE (
Process_Id SYSNAME NOT NULL,
SPID INT NOT NULL,
IsVictim BIT NOT NULL,
[Statement] SYSNAME NOT NULL,
[SqlHandle] VARBINARY(64) NULL,
[Text] NVARCHAR(4000) NULL,
Line INT NULL,
StmtStartOffset INT NULL,
StmtEndOffset INT NULL,
StatementNum INT NOT NULL,
PlanHandle VARBINARY(64) NULL,
QueryPlan XML NULL
);
INSERT @Statements
SELECT y.*, qs.plan_handle AS PlanHandle, pln.query_plan AS QueryPlan
FROM (
SELECT Process_Id = x.Process_Id,
SPID = x.SPID,
IsVictim= x.IsVictim,
[Statement] = y.XmlCol.value('(@procname)[1]', 'SYSNAME'),
[SqlHandle] = CONVERT(VARBINARY(64), y.XmlCol.value('(@sqlhandle)[1]', 'VARCHAR(128)'), 1),
[Text] = y.XmlCol.value('(text())[1]', 'NVARCHAR(4000)'),
Line = y.XmlCol.value('(@line)[1]', 'INT'),
StmtStartOffset = ISNULL(y.XmlCol.value('(@stmtstart)[1]', 'INT'), 0),
StmtEndOffset = ISNULL(y.XmlCol.value('(@stmtend)[1]', 'INT'), -1),
StatementNum = ROW_NUMBER() OVER(ORDER BY y.XmlCol DESC)
FROM #Processes x
OUTER APPLY x.CallStack.nodes('executionStack/frame') y(XmlCol)
) y
LEFT JOIN sys.dm_exec_query_stats qs ON y.SqlHandle = qs.sql_handle AND y.StmtStartOffset = qs.statement_start_offset AND y.StmtEndOffset = qs.statement_end_offset
OUTER APPLY sys.dm_exec_query_plan(qs.plan_handle) pln
SELECT s.SPID, s.IsVictim,
s.[Statement], s.[Text], s.Line,
s.QueryPlan, s.PlanHandle, s.SqlHandle,
s.Process_Id
FROM @Statements s
ORDER BY s.IsVictim DESC, s.StatementNum;
– Data access operators
WITH XMLNAMESPACES (
'http://www.w3.org/2001/XMLSchema-instance' AS xsi,
'http://www.w3.org/2001/XMLSchema' AS xsd,
DEFAULT 'http://schemas.microsoft.com/sqlserver/2004/07/showplan'
),
QueryPlans AS (
SELECT x.Process_Id, x.SPID, x.IsVictim, x.QueryPlan
FROM (
SELECT s.Process_Id, s.SPID, s.IsVictim, s.QueryPlan, ROW_NUMBER() OVER(PARTITION BY BINARY_CHECKSUM(CONVERT(NVARCHAR(MAX),s.QueryPlan)) ORDER BY @@SPID) AS RowNum
FROM @Statements s
WHERE s.QueryPlan IS NOT NULL
) x
WHERE x.RowNum = 1
)
SELECT b.SPID, b.IsVictim,
b.StatementType, b.[Statement],
b.LogicalOp, b.PhysicalOp,
b.ObjectName, b.IndexName, b.IndexKind,
b.Warnings, b.MissingIndexes,
b.Process_Id,
b.BatchId, b.StatementId, b.NodeId
FROM (
SELECT a.Process_Id, a.SPID, a.IsVictim,
a.BatchId, a.StatementId, a.NodeId,
a.StatementType, a.[Statement], /*a.ParamStatement,*/
LogicalOp = CASE
WHEN a.TableReferenceId = -1 AND a.IndexKind = N'Clustered' AND a.LogicalOp = 'Clustered Index Seek' THEN 'Key Lookup'
ELSE a.LogicalOp
END,
PhysicalOp = CASE
WHEN a.IndexKind = N'NonClustered' AND a.PhysicalOp IN('Clustered Index Insert', 'Table Insert') THEN 'Index Insert'
WHEN a.IndexKind = N'NonClustered' AND a.PhysicalOp IN('Clustered Index Update', 'Table Update') THEN 'Index Update'
WHEN a.IndexKind = N'NonClustered' AND a.PhysicalOp IN('Clustered Index Delete', 'Table Delete') THEN 'Index Delete'
WHEN a.IndexKind = N'NonClustered' AND a.PhysicalOp IN('Clustered Index Merge', 'Table Merge') THEN 'Index Merge'
ELSE a.PhysicalOp
END,
a.ObjectName, a.IndexName, a.IndexKind,
a.Warnings, a.MissingIndexes
FROM (
SELECT – batch_XmlCol = batch.XmlCol.query('.'),
BatchId = DENSE_RANK() OVER(ORDER BY batch.XmlCol),
StatementId = stmt.XmlCol.value('(@StatementId)[1]', 'INT'),
NodeId = oper.XmlCol.value('(@NodeId)[1]', 'INT'),
StatementType = stmt.XmlCol.value('(@StatementType)[1]', 'SYSNAME'),
[Statement] = stmt.XmlCol.value('(@StatementText)[1]', 'NVARCHAR(4000)'),
ParamStatement = stmt.XmlCol.value('(@ParameterizedText)[1]', 'NVARCHAR(4000)'),
LogicalOp = oper.XmlCol.value('(@LogicalOp)[1]', 'SYSNAME'),
PhysicalOp = oper.XmlCol.value('(@PhysicalOp)[1]', 'SYSNAME'),
[TableReferenceId]= objt.XmlCol.value('(@TableReferenceId)[1]', 'INT'),
[IndexKind] = objt.XmlCol.value('(@IndexKind)[1]', 'SYSNAME'),
[ObjectName] = ISNULL(objt.XmlCol.value('(@Database)', 'SYSNAME') + '.' + objt.XmlCol.value('(@Schema)', 'SYSNAME') + '.', '') + objt.XmlCol.value('(@Table)[1]', 'SYSNAME'),
[IndexName] = ISNULL(objt.XmlCol.value('(@Index)', 'SYSNAME'), ''),
Warnings = wrng.XmlCol.query('.'),
MissingIndexes = misx.XmlCol.query('.'),
Process_Id = xp.Process_Id,
SPID = xp.SPID,
IsVictim= xp.IsVictim
FROM QueryPlans xp
CROSS APPLY xp.QueryPlan.nodes('//Batch') batch(XmlCol)
CROSS APPLY batch.XmlCol.nodes('Statements/StmtSimple') stmt(XmlCol)
OUTER APPLY stmt.XmlCol.nodes('QueryPlan/Warnings') wrng(XmlCol)
OUTER APPLY stmt.XmlCol.nodes('QueryPlan/MissingIndexes') misx(XmlCol)
OUTER APPLY stmt.XmlCol.nodes('QueryPlan//RelOp') oper(XmlCol) – Operators
–OUTER APPLY oper.XmlCol.nodes('(.//Object)[1]') objt(XmlCol)
OUTER APPLY oper.XmlCol.nodes('./*/Object') objt(XmlCol)
) a
) b
WHERE b.PhysicalOp IN (
'Table Insert', 'Table Update', 'Table Delete', 'Table Merge', 'Table Scan',
'Clustered Index Insert', 'Clustered Index Update', 'Clustered Index Delete', 'Clustered Index Merge', 'Clustered Index Scan', 'Clustered Index Seek',
'Index Insert', 'Index Update', 'Index Delete', 'Index Merge', 'Index Scan', 'Index Seek',
'RID Lookup' – Key Lookup = Clustered Index Seek
) AND EXISTS (
SELECT *
FROM #Resources r
WHERE (r.ProcessOwner_Id = b.Process_Id OR r.ProcessWaiter_Id = b.Process_Id)
AND r.Resource_ObjectName = b.ObjectName
AND r.Resource_IndexName = b.IndexName
)
ORDER BY IsVictim DESC, SPID, BatchId, StatementId, NodeId;
END;
GO
Sample usage: Create a trace with SQL Profiler including [Deadlock graph event]. After intercepting a deadlock event you have to use [Extract event data] command to save on local computer the XML content of [Deadlock graph event] into a *.xdl file (ex. D:\DbEvents\deadlock18.xdl). Then, you have to execute the stored procedure:
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
EXEC dbo.sp_DeadlockAnalyzer @xdl;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
Note #1: The type of resource can be keylock or pagelock (at this moment).
Note #2: I’ve tested this procedure only with SQL Profiler – SQL Server 2012.
Note #3: This procedure should be executed on the same SQL instance.
Note #4: Next update will analyze also triggers execution plans. A future version will try to correlate data access operators and type of SQL statements (ex. SELECT -> S locks, UPDATE -> U or X lock on target table, etc.).
sp_DeadlockAnalyzer is a stored procedure created with one goal: to help DBAs to debug deadlocks in SQL Server. It has one parameter: @xdl which is the XML content from [deadlock graph event] intercepted with SQL Trace (SQL Profiler). It shows information about isolation level, blocked resources, connections and T-SQL statements with their execution plans. The stored procedure should be executed in the context of the original database (ex. SimpleDeadlock in my example).
Future plans:
Output:
Source code:
History:
2014-06-10 Small revision of source code (SUBSTRING: ISNULL, NULLIF)
– Test #1
WITH MyCTE
AS (
SELECT *, ROW_NUMBER()OVER(ORDER BY MyID) AS RowNum
FROM dbo.MyTable
)
SELECT *
FROM MyCTE crt
LEFT JOIN MyCTE prev ON crt.RowNum=prev.RowNum+1;
GO
In this case, for every row is displayed the previous row values:
Because a common table expression is a local view, the source code is expanded within the source of caller (ex. a INSERT, UPDATE, DELETE or a SELECT statement) and the execution plan for above SELECT statement looks like this:
As you can see from above execution plan, the common table expression (MyCTE) is expanded two times: crt and prev. If we want to see how many time is executed every “instance” of MyCTE then we can inspect the value of Number of Executions property for every operator. Bellow, you can see that for Number of Executions for Clustered Index Scan for crt is 1 and for prev is 5. Basically, in this example, the prev part is executed five times: one time for every row from crt part and overall the MyCTE is executed six times:
It’s possible to change this behavior ? Yes, it’s possible and the answer (at least one answer) comes from changing the physical type of JOIN which in this case is LOOP / NESTED LOOPS.
I’ve changed the physical join from LOOP to HASH and MERGE JOIN and the result was that the prev part is executed, in my example, just one time and overall the MyCTE is executed two times:
– Test #3
WITH MyCTE
AS (
SELECT *, ROW_NUMBER()OVER(ORDER BY MyID) AS RowNum
FROM dbo.MyTable
)
SELECT *
FROM MyCTE crt
LEFT MERGE JOIN MyCTE prev ON crt.RowNum=prev.RowNum+1;
GO
Another aspect is, also, interesting: in this small test, the number of logical reads is smaller for HASH and MERGE JOIN than for LOOP JOIN:
Note #1: because of join hints (LOOP, MERGE, HASH) the join order is enforced. Don’t use these hints without proper testing your queries. The usage of these hints without carefully analyzing the execution plans can lead to suboptimal plans.
Note #2: Starting with SQL Server 2012 you can use LAG function to get previous row’s values. See also LEAD function.
Files: Net_User_Group_Bucharest_2014-06-10.zip
Sometimes we need to test our apps for deadlocks. In this case we might need a simple way to simulate this type of error.
According to this, a deadlock occurs when two or more transactions block each other thus:
(source)
Resources: here (section Deadlock Information Tools, Resource attributes) and here (section Resource Details) you may find a full list with all resources types. Usually, resources which appear in deadlocks are KEYs and PAGes. A KEY refers to index records and this type of resource is identified by a hash value (hash). A PAGe refers to 8K data pages and this type of resource is indentified by a pair <file_id>:<page_in_file>.
Example:
USE SimpleDeadlock;
GO
CREATE TABLE dbo.MyTable (
Col1 INT NOT NULL CONSTRAINT PK_MyTable_Col1 PRIMARY KEY (Col1),
Col2 VARCHAR(50) NOT NULL
);
GO
INSERT dbo.MyTable (Col1, Col2) VALUES (1, 'A');
INSERT dbo.MyTable (Col1, Col2) VALUES (2, 'B');
GO
Note: for every PRIMARY KEY constraint SQL Server automatically creates a UNIQUE INDEX (clustered or non-clustered). In this case, SQL Server creates an UNIQUE index (PK_MyTable_Col1) for the primary key constraint.
In order to get information about the hash value of every record within index PK_MyTable_Col1 we could use the following query:
Note: %%lockres%% function is undocumented.
Because this table is small it consumes only one 8K page. We can get information about this page by using sp_AllocationMetadata stored procedure (or by using sys.system_internals_allocation_units view):
As you can see, this page is 283 within file 1.
Lock modes (types) are described here and lock compatibility is described here. For example:
In this example we will simulate a write – read deadlock thus:
Using SQL Server Management Studio, open a new window (SQLQuery1.sql) and start a transaction T1 thus
At this moment only one resourcea0) is locked X by transaction T1:
Now, open a new window (SQLQuery2.sql) and initiate a new transaction (T2):
SELECT *
FROM dbo.MyTable
WHERE Col1 = 1;
– End of Step #2
At this moment, UPDATE statement will take an X lock on record 2 B and SELECT will try to take a S lock on record 1 A (which is already locked by T1). Because record 1 A is already locked X by T1, T2 will have to wait till this X lock is released:
Now, we have to return in the first window (SQLQuery1.sql) and execute
In this case, SELECT statement from transaction T1 will try to take a S lock on record 2 B
Because this record is already locked X by T2 and because S and X locks are incompatible, T1 which request a S lock will have to wait till X lock (T2) are released.
In this moment the circle is completed and SQL Server deadlock monitor will automatically detect and end this deadlock:
In my case, SQL Server selected transaction T2 as deadlock victim. This means that T2 is automatically cancelled (ROLLBACK) and all locks took by this transaction are released.
Note: transaction isolation level used for this example is read committed (default).
Files: Net_User_Group_Bucharest_2014-04-08.zip
SQL Server Query optimizer can automatically remove joins between parent and child tables if the query’s result remains unchanged. This is optimization is called foreign key join elimination.
Bellow example creates two tables with a simple foreign key defined on a mandatory column:
CREATE TABLE dbo.Customer (
CustomerID INT PRIMARY KEY,
FirstName NVARCHAR(50) NOT NULL,
LastName NVARCHAR(50) NOT NULL
);
GO
CREATE TABLE dbo.SalesOrder (
SalesOrder INT PRIMARY KEY,
OrderDate DATE NOT NULL,
CustomerID INT NOT NULL
CONSTRAINT FK_SalesOrder_CustomerID
REFERENCES dbo.Customer(CustomerID),
TotalAmount NUMERIC(18,2) NOT NULL
);
GO
For the following query
SQL Server generates an execution plan (SSMS: Query > Display estimated execution plan) which includes a single data access operator (Clustered Index Scan on child table dbo.SalesOrder):
In above example, Query Optimizer automatically removes the JOIN between parent table dbo.Customer and child table dbo.SalesOrder and, also, it removes data access operator on parent table dbo.Customer:
This optimization is possible, mainly, because the foreign key constraint is defined on a single mandatory column and this constraint is trusted.
I think it worth mentioning those reasons which prevent Query Optimized to apply this optimization:
SELECT fk.is_disabled, fk.is_not_for_replication, fk.is_not_trusted FROM sys.foreign_keys fk
WHERE fk.name = N'FK_SalesOrder_CustomerID';
GO
/*
is_disabled is_not_for_replication is_not_trusted
———– ———————- ————–
1 0 1
*/
SELECT COUNT(*)
FROM dbo.SalesOrder so INNER JOIN dbo.Customer c ON so.CustomerID = c.CustomerID
GO
– Test #2 Disabling and enabling FK constraint.
– What happens if FK constraint is enabled but is not trusted and is not marked [NOT FOR REPLICATION]?
– FK constraint is enabled and optimization is allowed
SELECT fk.is_disabled, fk.is_not_for_replication, fk.is_not_trusted FROM sys.foreign_keys fk
WHERE fk.name = N'FK_SalesOrder_CustomerID';
GO
/*
is_disabled is_not_for_replication is_not_trusted
———– ———————- ————–
0 0 0
*/
– We disable the FK constraint
ALTER TABLE dbo.SalesOrder
NOCHECK CONSTRAINT FK_SalesOrder_CustomerID;
GO
– Now, the FK constraint is disabled and becomes not trusted. Optimization is not possible.
SELECT fk.is_disabled, fk.is_not_for_replication, fk.is_not_trusted FROM sys.foreign_keys fk
WHERE fk.name = N'FK_SalesOrder_CustomerID';
GO
/*
is_disabled is_not_for_replication is_not_trusted
———– ———————- ————–
1 0 1
*/
SELECT COUNT(*)
FROM dbo.SalesOrder so INNER JOIN dbo.Customer c ON so.CustomerID = c.CustomerID
GO
The execution plans includes data access operator for both tables:
SELECT fk.is_disabled, fk.is_not_for_replication, fk.is_not_trusted FROM sys.foreign_keys fk
WHERE fk.name = N'FK_SalesOrder_CustomerID';
GO
/*
is_disabled is_not_for_replication is_not_trusted
———– ———————- ————–
0 0 1
*/
SELECT COUNT(*)
FROM dbo.SalesOrder so INNER JOIN dbo.Customer c ON so.CustomerID = c.CustomerID
GO
The execution plan is the same:
SELECT fk.is_disabled, fk.is_not_for_replication, fk.is_not_trusted FROM sys.foreign_keys fk
WHERE fk.name = N'FK_SalesOrder_CustomerID';
GO
/*
is_disabled is_not_for_replication is_not_trusted
———– ———————- ————–
0 0 0
*/
SELECT COUNT(*)
FROM dbo.SalesOrder so INNER JOIN dbo.Customer c ON so.CustomerID = c.CustomerID
GO
And the execution plan shows only one data access operator:
To check what FK constraints allows FK join elimination optimization I wrote this script:
isn’t safe from concurrency point of view if we use default setting for transactions isolation level: read committed.
This means that
The only difference between this test and previous test is the next index:
The ostress.exe tool is executed with the same parameters:
The parameter -o”d:\SqlOstressOutput” represents the output folder. This folder is used to write the results of every T-SQL script and connection plus ostress.log file which collects overall results.
This time, this unique index prevents insertion of duplicate emails:
and, also, D:\SqlOstressOutput\ostress.log contains some errors generated by T-SQL scripts because at some moments they try to insert duplicate emails (see lines 34 – 46) but IUN_Customer_Email index prevents duplicate emails :
Solutions: to prevent these concurrency problems here I’ve presented few solutions. Please bear in mind that you should rigorous test selected solution. Next blog will present pros and cons for every solution.
Is it safe IF EXISTS – UPDATE ELSE INSERT [UPSERT] ?
Is it safe IF EXISTS – UPDATE ELSE INSERT [UPSERT] ? #2