AI SQL Tuner Recommendations

Tuning Goal: Locking and Blocking Analysis
Server: RockyPC
Database: tpch10
Version: Microsoft SQL Server 2022 (RTM-GDR) (KB5084815) - 16.0.1175.1 (X64)
Edition: Developer Edition (64-bit), Engine Edition: Enterprise

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.

  1. Top priority: remove or redesign the trigger work so it does not scan/re-read dbo.orders inside the user transaction.
  2. Second priority: batch the date-range order updates into smaller commits and avoid long page-lock accumulation.
  3. 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.
  4. 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.

Blocking Active
Yes
Lead Blocker Sessions
62, 69
Distinct Blocked Sessions
3
Longest Lock Wait
25,118 ms
Observed Cumulative Wait
~63,733 ms
Recent Deadlock in tpch10
Yes

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

  1. 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%
  2. 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%
  3. 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%
  4. 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%
  5. 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%
  6. 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.