AI SQL Tuner Recommendations
Executive Summary
Blocking is active and confirmed. The dominant pattern is a hotspot on dbo.orders / index o_orderdate_ind, driven by a large date-range UPDATE and an AFTER trigger that immediately runs a MERGE back against dbo.orders to recompute customer aggregates. This creates mutual lock pressure on the same index pages and has already produced at least one recent deadlock in tpch10.
- Top priority: remove or redesign the trigger work so it does not scan/re-read dbo.orders inside the user transaction.
- Second priority: batch the date-range order updates into smaller commits and avoid long page-lock accumulation.
- Third priority: enable READ_COMMITTED_SNAPSHOT for tpch10 to reduce reader/writer blocking from trigger reads, while recognizing it will not remove writer/writer contention.
- Fourth priority: add a supporting index for the trigger aggregation path on orders(o_custkey, o_orderdate); confirm final design with the Index Tuning goal for the specific schema and table.
Most important blocker: session 62 running trigger statement MERGE dbo.CustomerOrderStats..., because it blocks two UPDATE sessions and is part of a bidirectional blocking cycle with session 69.
Environment
- SQL Server 2022 Enterprise capabilities are available, including Intelligent Query Processing, Parameter Sensitive Plan optimization, and online operations.
- RCSI is OFF and Snapshot Isolation is OFF.
- Table lock escalation is set to TABLE for orders, lineitem, and related tables.
- Query Store is enabled and provided useful historical evidence.
- Extended Events deadlock history is available and already captured deadlock evidence.
Blocking Status
Conclusion: blocking is active.
- BlockingChains: session 62 blocks session 84 on LCK_M_X for 25,118ms.
- BlockingChains: session 62 blocks session 69 on LCK_M_X for 23,127ms.
- BlockingChains: session 69 blocks session 62 on LCK_M_S for 1,403ms, showing bidirectional contention / deadlock-prone behavior.
- LockInventory: waiting locks are on PAGE resources for tpch10.orders, index o_orderdate_ind, with session 62 waiting on S and session 69 waiting on X.
- Long-Running Transactions: user transactions for sessions 69 and 84 have been open for 28-29 seconds.
Blocking Tree
Observed blocking tree:
Session 62 - MERGE in trigger dbo.trg_orders_UpdateCustomerStats
Statement:
WITH ChangedCustomers AS (...)
MERGE dbo.CustomerOrderStats AS tgt
USING (
SELECT o.o_custkey, COUNT(*), MAX(o.o_orderdate)
FROM dbo.orders AS o
JOIN ChangedCustomers AS c ON c.o_custkey = o.o_custkey
GROUP BY o.o_custkey
) AS src ...
├─ blocks Session 84 - UPDATE dbo.orders ... WHERE o_orderdate BETWEEN @FromDate AND @ToDate
└─ blocks Session 69 - UPDATE dbo.orders ... WHERE o_orderdate BETWEEN @FromDate AND @ToDate
Session 69 - UPDATE dbo.orders ... WHERE o_orderdate BETWEEN @FromDate AND @ToDate
└─ blocks Session 62 - trigger MERGE waiting for S lock on PAGE in orders.o_orderdate_ind
- Lead blocker by fan-out: session 62.
- Mutual blocker in cycle: session 69.
- Most important waiter statement: session 84 running UPDATE o SET o.o_comment = LEFT(CONCAT(...),79) FROM dbo.orders AS o WHERE o.o_orderdate BETWEEN @FromDate AND @ToDate.
| Role | Session | Statement | Evidence |
|---|---|---|---|
| Primary blocker | 62 | Trigger MERGE dbo.CustomerOrderStats | BlockingChains shows 62 blocking 84 and 69; LockInventory shows session 62 holding/granting many X locks on orders.o_orderdate_ind. |
| Competing blocker | 69 | UPDATE dbo.orders ... WHERE o_orderdate BETWEEN @FromDate AND @ToDate | BlockingChains shows 69 blocking 62 on LCK_M_S; LockInventory shows session 69 with 10,771 granted X page locks on the same index. |
| Blocked waiter | 84 | Same UPDATE procedure | BlockingChains shows session 84 waiting on LCK_M_X for 25,118ms. |
Root Cause Classification
- High Confidence Hotspot table or index — concentrated contention on orders / o_orderdate_ind.
- Evidence: LockInventory repeatedly shows page locks on orders, index o_orderdate_ind, with waiting S and X requests and tens of thousands of granted X locks.
- Evidence: Both the UPDATE and the trigger MERGE touch dbo.orders in the same transaction window.
- High Confidence Long-running transaction — the user transaction stays open while trigger work executes.
- Evidence: Long-Running Transactions shows session 69 at 29s and session 84 at 28s.
- Evidence: Waiter batch context explicitly includes BEGIN TRAN in dbo.usp_UpdateOrdersAndLineitem.
- High Confidence Missing index causing lock amplification — no DMV recommendation appeared, but workload evidence strongly suggests a missing support path for the trigger aggregation by customer.
- Evidence: Trigger statement joins ChangedCustomers to dbo.orders on o_custkey and aggregates COUNT(*), MAX(o_orderdate).
- Evidence: IndexUsage shows orders.o_orderdate_ind exists, but no evidence of an efficient customer-based access path; Query Store for query 480 shows 12,966,375 and 4,862,472 logical reads for the trigger MERGE plans.
- Medium Confidence Lock escalation risk — not explicitly observed as a granted table X lock in the snapshot, but escalation settings and page-lock volume materially increase risk.
- Evidence: Database Settings shows orders lock escalation = TABLE.
- Evidence: LockInventory shows session 69 with 10,771 X page locks and session 62 with thousands more on the same index.
- Medium Confidence TempDB contention masquerading as blocking — not the primary issue.
- Evidence against primary TempDB cause: blocking waits are LCK_M_X and LCK_M_S on orders.o_orderdate_ind.
- Evidence of secondary contribution: TempDB shows heavy spill activity, including 75,221 spills for related MERGE/UPDATE queries, which can prolong transaction duration and therefore amplify blocking.
- Low Confidence Parameter sniffing causing large scans — possible but not provable from provided data because blocker plan XML is unavailable.
Impact Metrics
| Metric | Value | Basis |
|---|---|---|
| Total sessions blocked | 3 distinct sessions observed | Sessions 62, 69, 84 appear as waiters in BlockingChains. |
| External blocked sessions | 2 | Sessions 69 and 84 are blocked by session 62; session 62 is also blocked by 69 in a cycle. |
| Longest wait | 25,118 ms | BlockingChains waiter session 84, LCK_M_X. |
| Observed cumulative wait | ~63,733 ms | 25,118 + 23,127 + 1,403 + 1,402 + 1,352 + 1,331 ms from explicit lock waits shown. |
| Transaction age | 28-29 seconds | Long-Running Transactions section. |
| Business impact estimate | Moderate to High | Concurrent batch maintenance/update procedures on a 15M-row orders table are serialized by trigger activity; recent deadlock indicates user-visible failures and retries are likely. |
Detailed Prioritized Recommendations
-
Redesign or remove the synchronous trigger aggregate maintenance on dbo.orders
- Fix: Replace dbo.trg_orders_UpdateCustomerStats synchronous MERGE dbo.CustomerOrderStats logic with either:
- an asynchronous queue/process, or
- a narrow staging table of changed customer keys processed after commit, or
- a direct delta-based update that does not rescan dbo.orders.
- Rationale: The trigger is the primary lead blocker and re-reads the hot table inside the same transaction.
- BlockingChains: session 62 trigger MERGE blocks sessions 84 and 69.
- BlockingChains: session 69 simultaneously blocks session 62, proving the trigger and base UPDATE are contending with each other.
- Query Store: trigger query 480 consumed up to 12,966,375 logical reads and 24,069ms duration for one plan, indicating large work for post-DML trigger logic.
- LockInventory: trigger-related locks are on orders.o_orderdate_ind, the same hotspot resource as the base UPDATE.
- Estimated risk: Medium. Functional behavior changes if downstream code expects immediate stats maintenance.
- Rollback plan: keep the existing trigger definition scripted; deploy alternative process behind feature flag or staged rollout; re-enable original trigger if validation fails.
- Confidence: High
- Estimated reduction in blocking: 55% to 75%
- Fix: Replace dbo.trg_orders_UpdateCustomerStats synchronous MERGE dbo.CustomerOrderStats logic with either:
-
Batch the date-range UPDATE dbo.orders into smaller commits
- Fix: Rewrite dbo.usp_UpdateOrdersAndLineitem to update orders in chunks by key or date/key ranges, committing every small batch.
- Rationale: Current code holds a single transaction for the whole window and accumulates many page X locks.
- Batch context: procedure explicitly says BEGIN TRAN and performs multiple logical steps.
- LockInventory: session 69 holds 10,771 granted X page locks on orders.o_orderdate_ind.
- Query Store: query 476 (UPDATE dbo.orders) shows 154,801ms total duration and 19,024,647 logical reads.
- Estimated risk: Low to Medium. More commits may affect atomicity semantics and downstream expectations.
- Rollback plan: retain original stored procedure definition; revert procedure if chunking causes unacceptable application behavior.
- Confidence: High
- Estimated reduction in blocking: 35% to 60%
-
Add a support index for trigger/customer aggregation on dbo.orders
- Fix: Test an index such as CREATE INDEX IX_orders_custkey_orderdate ON dbo.orders(o_custkey, o_orderdate).
- Rationale: The trigger groups by o_custkey and computes MAX(o_orderdate), but the hotspot access path shown is the date index, not a customer-driven path.
- Blocker Statement: trigger query joins ChangedCustomers to dbo.orders AS o ON c.o_custkey = o.o_custkey and groups by o.o_custkey.
- IndexUsage: only notable orders index usage shown is o_orderdate_ind with row count about 15,000,000.
- Query Store: trigger MERGE is expensive in reads, suggesting broad re-access to orders.
- Important note: No missing-index DMV recommendation was present, so validate with testing. Also confirm final design by running the Index Tuning goal specifically for schema/table dbo.orders for more comprehensive analysis.
- Estimated risk: Medium. Additional write overhead and storage usage.
- Rollback plan: create nonclustered index online if possible; if write overhead is too high, drop the index.
- Confidence: Medium
- Estimated reduction in blocking: 20% to 45%
-
Enable READ_COMMITTED_SNAPSHOT for tpch10
- Fix: Enable row-versioned read committed isolation.
- Rationale: The trigger MERGE requests shared locks while updates hold exclusive locks. RCSI can remove many read/write conflicts.
- Database Settings: RCSI is OFF.
- BlockingChains: session 62 waits on LCK_M_S behind session 69's update.
- LockInventory: session 62 has waiting S lock on orders.o_orderdate_ind.
- Limit: this will not eliminate writer/writer blocking such as update vs update, but it can materially reduce trigger read blocking.
- Estimated risk: Medium. Requires application review for row-versioning semantics and tempdb capacity planning.
- Rollback plan: disable RCSI if application behavior is impacted; monitor tempdb versioning after enablement.
- Confidence: High
- Estimated reduction in blocking: 15% to 35%
-
Reduce or eliminate use of MERGE in triggers and maintenance statements
- Fix: Replace MERGE with separate UPDATE/INSERT statements against pre-aggregated input.
- Rationale: In this workload, MERGE is part of the blocking path and historical deadlock path.
- BlockingChains: lead blocker command is MERGE.
- Long-Running Transactions: one transaction is reported under trigger SQL/command MERGE.
- Query Store: multiple MERGE statements are among top consumers, including query 470 and query 480.
- Deadlock History: recent deadlock in tpch10 occurred during the same workload window and involved page waits in a user transaction.
- Estimated risk: Medium. Requires code change and regression test for correctness.
- Rollback plan: preserve original MERGE-based modules; revert if post-change totals diverge.
- Confidence: Medium
- Estimated reduction in blocking: 15% to 30%
-
Address spill-heavy plans to shorten transaction time
- Fix: tune the spill-heavy statements, especially the lineitem update and trigger MERGE paths, by improving join/index access, considering smaller batches, and validating memory grant quality.
- Rationale: Spills are not the root lock cause but clearly extend statement runtime and lock duration.
- TempDB Contention: top spilling queries include the trigger-style MERGE and lineitem UPDATE with 75,221 spills.
- Query Store: lineitem update queries have extreme logical reads, up to 76,510,570.
- Estimated risk: Low to Medium.
- Rollback plan: revert individual tuning changes if regressions appear.
- Confidence: Medium
- Estimated reduction in blocking: 10% to 20%
Code / Query Rewrite Guidance
1) Rewrite the orders update into key-based chunks
Before
BEGIN TRAN;
UPDATE o
SET o.o_comment = LEFT(CONCAT(
o.o_comment,
' | BATCH_UPDATE ',
CONVERT(varchar(10), @FromDate, 120),
'-',
CONVERT(varchar(10), @ToDate, 120)
), 79)
FROM dbo.orders AS o
WHERE o.o_orderdate BETWEEN @FromDate AND @ToDate;
-- more work here...
COMMIT;
After
DECLARE @BatchSize int = 5000;
WHILE 1 = 1
BEGIN
;WITH c AS
(
SELECT TOP (@BatchSize) o.o_orderkey
FROM dbo.orders AS o
WHERE o.o_orderdate BETWEEN @FromDate AND @ToDate
AND o.ProcessedFlag = 0 -- example batching predicate
ORDER BY o.o_orderkey
)
BEGIN TRAN;
UPDATE o
SET o.o_comment = LEFT(CONCAT(
o.o_comment,
' | BATCH_UPDATE ',
CONVERT(varchar(10), @FromDate, 120),
'-',
CONVERT(varchar(10), @ToDate, 120)
), 79),
o.ProcessedFlag = 1
FROM dbo.orders AS o
JOIN c ON c.o_orderkey = o.o_orderkey;
IF @@ROWCOUNT = 0
BEGIN
ROLLBACK;
BREAK;
END
COMMIT;
END
- Use a stable batching key such as o_orderkey.
- Commit each batch to release locks early.
- If a processing flag is not acceptable, use a temp table of target keys or a driving key-range approach.
2) Rewrite trigger maintenance to avoid rescanning orders in the same transaction
Current blocker pattern
WITH ChangedCustomers AS (
SELECT o_custkey FROM inserted
UNION
SELECT o_custkey FROM deleted
)
MERGE dbo.CustomerOrderStats AS tgt
USING (
SELECT o.o_custkey,
COUNT(*) AS order_count,
MAX(o.o_orderdate) AS last_orderdate
FROM dbo.orders AS o
JOIN ChangedCustomers AS c
ON c.o_custkey = o.o_custkey
GROUP BY o.o_custkey
) AS src
ON tgt.c_custkey = src.o_custkey
...
Safer pattern: stage changed customers and process after commit
CREATE TABLE dbo.CustomerOrderStatsQueue
(
c_custkey int NOT NULL PRIMARY KEY,
queued_at datetime2 NOT NULL DEFAULT sysdatetime()
);
-- Trigger keeps only narrow writes
INSERT INTO dbo.CustomerOrderStatsQueue(c_custkey)
SELECT DISTINCT o_custkey FROM inserted
UNION
SELECT DISTINCT o_custkey FROM deleted;
Then process asynchronously
;WITH src AS
(
SELECT q.c_custkey,
COUNT_BIG(*) AS order_count,
MAX(o.o_orderdate) AS last_orderdate
FROM dbo.CustomerOrderStatsQueue AS q
JOIN dbo.orders AS o
ON o.o_custkey = q.c_custkey
GROUP BY q.c_custkey
)
UPDATE tgt
SET tgt.order_count = src.order_count,
tgt.last_orderdate = src.last_orderdate
FROM dbo.CustomerOrderStats AS tgt
JOIN src ON src.c_custkey = tgt.c_custkey;
INSERT INTO dbo.CustomerOrderStats(c_custkey, order_count, last_orderdate)
SELECT src.c_custkey, src.order_count, src.last_orderdate
FROM src
WHERE NOT EXISTS
(
SELECT 1
FROM dbo.CustomerOrderStats AS tgt
WHERE tgt.c_custkey = src.c_custkey
);
- This keeps the OLTP transaction narrow.
- It removes immediate read-after-write pressure on the hot orders index pages.
3) Support the trigger path with an index
CREATE INDEX IX_orders_custkey_orderdate
ON dbo.orders (o_custkey, o_orderdate);
- Purpose: improve customer-based aggregation and reduce broad scans/reads.
- Validate this candidate with the Index Tuning goal for dbo.orders before finalizing.
Historical Deadlocks & Long-Term Mitigations
Recurring pattern is confirmed.
- Deadlock History: a deadlock occurred at 2026-05-05 00:35:47 UTC in tpch10 during the same workload window, involving PAGE wait resources and a user transaction under read committed.
- BlockingChains + LockInventory: current waits are also page-level on orders.o_orderdate_ind, so this is not an isolated one-off.
- Query Store: the top resource consumers are the same family of batch UPDATE and MERGE maintenance statements.
Long-term mitigations
- Move summary maintenance to asynchronous processing or scheduled ETL-style refresh.
- Keep maintenance operations for orders and lineitem in separate, shorter transactions when business rules permit.
- Consider application-level serialization for overlapping date windows if concurrency on the same date range is expected.
- Use retry logic for deadlock victims, but only after reducing the lock cycle root cause.
- Review whether the trigger-based design duplicates work already performed by the top Query Store MERGE maintenance statements for OrderTotals and CustomerOrderStats.
Monitoring & Evidence Quality
- Query Store: enabled and valuable here; it proved the recurring high-read statements and should remain on.
- Deadlock capture: already available through Extended Events / system_health; keep it enabled.
- BlockerPlans: no blocker plans found at collection time. Additional actual/estimated plans for session 62 and 69 would help confirm whether customer-based scans, memory grants, or parallel hash operations are driving the long trigger runtime.
- TempDB: current latch profile does not indicate primary TempDB allocation contention masquerading as blocking; PAGELATCH waits are small, and version store is 0.0MB. The main issue is lock contention on user table pages, with spills acting as a duration multiplier.