Executive summary Top priorities
Primary bottleneck observed
Large, scan-heavy analytical workloads on dbo.lineitem and dbo.orders (multi-million logical reads per execution).
Most impactful structural change
Fix dbo.orders heap (create clustered index/PK) and add 2–3 targeted nonclustered indexes to support the highest-cost plan-cache query patterns.
Columnstore suitability
dbo.lineitem (60M) and dbo.orders (15M) are strong candidates for nonclustered columnstore to accelerate aggregations and large scans with minimal OLTP overhead (user_updates shown as 0).
- Create a clustered index (PK) on dbo.orders (table is currently a heap with scans and lookups). High impactLow maintenance risk (updates=0)
- Add a nonclustered columnstore on dbo.lineitem to accelerate GROUP BY/aggregate-heavy TPC-H style queries (e.g., Q1, Q7/8/9-like). High impactExtra storage
- Add 2 targeted rowstore nonclustered indexes on dbo.lineitem to eliminate repeated large scans for order-based semi-joins and late-receipt predicates. High impactAdds write overhead if workload becomes OLTP
- Add rowstore nonclustered indexes on dbo.orders for date-range filters and customer joins used heavily in cache. Medium-high impact
- Update statistics (fullscan on largest tables initially) and enable async stats updates for less blocking on first-run. Medium impact
- Query rewrites: replace legacy comma joins + IN(subquery) and correlated EXISTS patterns with JOIN/APPLY forms to improve cardinality/plan shape and reduce memory grants. Medium impact
Detailed prioritized recommendations
-
Fix dbo.orders heap by creating a clustered primary key on (o_orderkey)
Priority 1
- Why: Index usage shows dbo.orders has a HEAP with 110 scans and 10 lookups; multiple plan-cache queries join on
o_orderkeyand filter ono_orderdate. Heaps amplify lookups and scanning costs. - Overhead: One-time build cost. Ongoing maintenance is low in your captured workload (user_updates=0).
- Implementation note: Because there is already a PK named
orders_pkcurrently implemented as NONCLUSTERED, the cleanest approach is to drop and recreate as CLUSTERED. If any foreign keys reference it, drop/recreate those as well (your provided constraint list does not show any table referencing orders exceptdbo.lineitem (lineitem_order_fk)which referencesdbo.orders.o_orderkey—so we must handle that FK).
USE [tpch]; GO /* Drop FK(s) referencing dbo.orders PK */ ALTER TABLE dbo.lineitem DROP CONSTRAINT lineitem_order_fk; GO /* Drop existing PK (currently nonclustered per usage stats) */ ALTER TABLE dbo.orders DROP CONSTRAINT orders_pk; GO /* Recreate PK as CLUSTERED */ ALTER TABLE dbo.orders ADD CONSTRAINT orders_pk PRIMARY KEY CLUSTERED (o_orderkey) WITH ( FILLFACTOR = 98, DATA_COMPRESSION = PAGE, ONLINE = ON, SORT_IN_TEMPDB = ON ); GO /* Recreate FK */ ALTER TABLE dbo.lineitem ADD CONSTRAINT lineitem_order_fk FOREIGN KEY (l_orderkey) REFERENCES dbo.orders(o_orderkey); GO - Why: Index usage shows dbo.orders has a HEAP with 110 scans and 10 lookups; multiple plan-cache queries join on
-
Create a nonclustered columnstore index (NCCI) on dbo.lineitem
Priority 2
- Why:
dbo.lineitemis 60M rows and the plan cache shows repeated, scan-heavy aggregations (e.g., Q1-style aggregation byl_returnflag, l_linestatus, and revenue calculations). NCCI provides batch-mode execution and compression benefits for these analytics patterns. - Overhead: Adds storage and delta-store maintenance; but your captured user_updates=0 indicates minimal DML, making this a strong fit.
- Notes: Keep the existing clustered rowstore index (
l_shipdate_ind) for selective date predicates; NCCI complements it for broad scans/aggregations.
USE [tpch]; GO CREATE NONCLUSTERED COLUMNSTORE INDEX NCCX_lineitem_analytics ON dbo.lineitem ( l_orderkey, l_partkey, l_suppkey, l_shipdate, l_commitdate, l_receiptdate, l_returnflag, l_linestatus, l_quantity, l_extendedprice, l_discount, l_tax, l_shipmode, l_shipinstruct ) WITH (ONLINE = ON); GO - Why:
-
Create a supporting rowstore nonclustered index for order-based aggregation/semi-join on dbo.lineitem
Priority 3
- Targets: The most expensive cached query pattern repeatedly executes:
o_orderkey IN (SELECT l_orderkey FROM lineitem GROUP BY l_orderkey HAVING SUM(l_quantity) > ...)and then joins back tolineitemonl_orderkey. - Why: Missing indexes repeatedly suggest
(l_orderkey) INCLUDE (l_discount, l_extendedprice, l_suppkey, l_partkey, l_commitdate, l_receiptdate). Consolidate into one index to cover the highest-frequency join + select list needs while keeping width bounded. - Overhead: Wider include list increases storage; however user_updates=0 on lineitem implies low maintenance cost in this workload snapshot.
USE [tpch]; GO CREATE INDEX IX_lineitem_orderkey_cover ON dbo.lineitem (l_orderkey) INCLUDE (l_quantity, l_extendedprice, l_discount, l_suppkey, l_partkey, l_commitdate, l_receiptdate, l_returnflag) WITH ( FILLFACTOR = 98, DATA_COMPRESSION = PAGE, ONLINE = ON, SORT_IN_TEMPDB = ON ); GO - Targets: The most expensive cached query pattern repeatedly executes:
-
Create a selective rowstore nonclustered index for late receipt/commit predicates on dbo.lineitem
Priority 4
- Targets: Supplier “numwait” query pattern uses:
l_receiptdate > l_commitdate, joins onl_orderkeyandl_suppkey, and probes for existence of other suppliers per order. - Why: A composite index starting with
l_orderkeythenl_suppkeysupports both the main join and the correlated existence checks. Including the date columns supports the predicate evaluation without extra lookups. - Overhead: Additional nonclustered index on a 60M row table; justified by very high logical reads (~3.9M) and CPU (~26–35s) per execution for this pattern.
USE [tpch]; GO CREATE INDEX IX_lineitem_order_supp_receipt_commit ON dbo.lineitem (l_orderkey, l_suppkey) INCLUDE (l_receiptdate, l_commitdate) WITH ( FILLFACTOR = 98, DATA_COMPRESSION = PAGE, ONLINE = ON, SORT_IN_TEMPDB = ON ); GO - Targets: Supplier “numwait” query pattern uses:
-
Create date-range and join-supporting nonclustered indexes on dbo.orders
Priority 5
- Targets:
- Revenue/top-N and region/nation revenue queries filter on
o_orderdaterange and join ono_custkey, projecto_orderkey. - Orderpriority counts filter on
o_orderdateand group byo_orderpriority. - Top-10 revenue query projects
o_shippriorityando_orderdategrouped byl_orderkey/o_orderkey.
- Revenue/top-N and region/nation revenue queries filter on
- Why: Missing index hints point to
o_custkey+o_orderdatepatterns and pureo_orderdaterange patterns. - Overhead: Moderate; orders is 15M rows. Still acceptable given user_updates=0 in captured stats.
USE [tpch]; GO /* For customer + date range patterns */ CREATE INDEX IX_orders_cust_orderdate ON dbo.orders (o_custkey, o_orderdate) INCLUDE (o_orderkey, o_totalprice) WITH ( FILLFACTOR = 98, DATA_COMPRESSION = PAGE, ONLINE = ON, SORT_IN_TEMPDB = ON ); GO /* For pure date-range analytics and grouping/projection */ CREATE INDEX IX_orders_orderdate ON dbo.orders (o_orderdate) INCLUDE (o_orderkey, o_custkey, o_shippriority, o_orderpriority, o_orderstatus) WITH ( FILLFACTOR = 98, DATA_COMPRESSION = PAGE, ONLINE = ON, SORT_IN_TEMPDB = ON ); GO - Targets:
-
Create a nonclustered index on dbo.partsupp for ps_suppkey lookups
Priority 6
- Why: Missing index recommends
(ps_suppkey) INCLUDE (ps_supplycost, ps_availqty). Whilepartsupphas a clustered PK (likely on (ps_partkey, ps_suppkey)), supplier-driven access will not efficiently seek unlessps_suppkeyis leading. - Overhead: Adds one NC index to an 8M row table; acceptable in analytics-oriented workloads.
- Guardrail: If supplier-driven access is rare, deprioritize; but the missing index shows 10 seeks/scans with meaningful impact.
USE [tpch]; GO CREATE INDEX IX_partsupp_suppkey ON dbo.partsupp (ps_suppkey) INCLUDE (ps_supplycost, ps_availqty, ps_partkey) WITH ( FILLFACTOR = 98, DATA_COMPRESSION = PAGE, ONLINE = ON, SORT_IN_TEMPDB = ON ); GO - Why: Missing index recommends
-
Consider nonclustered columnstore on dbo.orders (optional, analytics-heavy workloads)
Priority 7
- Why: 15M-row table with frequent date-range scans and groupings; NCCI can improve batch-mode joins/aggregations with lineitem and customer/nation/region.
- Overhead: Extra storage; delta store overhead on DML (currently appears negligible).
- When to skip: If orders is frequently inserted/updated (not reflected in your usage snapshot), prefer rowstore only.
USE [tpch]; GO CREATE NONCLUSTERED COLUMNSTORE INDEX NCCX_orders_analytics ON dbo.orders ( o_orderkey, o_custkey, o_orderdate, o_totalprice, o_orderpriority, o_shippriority, o_orderstatus ) WITH (ONLINE = ON); GO -
Drop/avoid redundant indexes (none recommended to drop from provided list)
Priority 8
- dbo.lineitem: Existing clustered index
l_shipdate_indshows seeks (70) and scans (130) with 0 updates; keep it. It supports Q1-stylel_shipdate <= ...predicates. - dbo.orders: Current
orders_pkis used (10 seeks). After converting PK to clustered, no separate nonclustered PK should remain. - No additional drops are recommended because updates are 0 and existing indexes appear either essential (PKs) or actively used.
- dbo.lineitem: Existing clustered index
-
Statistics recommendations
Priority 9
- Why: Analytical queries are sensitive to cardinality. After large index changes and on large tables, proactively updating stats improves plan quality and reduces memory grant over-allocation.
- Settings: Auto-create/update stats are enabled; consider enabling async stats updates to reduce blocking on first compile after stale stats.
USE [tpch]; GO /* Enable async stats updates to reduce compile-time waits on large tables */ ALTER DATABASE [tpch] SET AUTO_UPDATE_STATISTICS_ASYNC ON; GO /* One-time post-change stats refresh (target the largest tables) */ UPDATE STATISTICS dbo.lineitem WITH FULLSCAN; UPDATE STATISTICS dbo.orders WITH FULLSCAN; UPDATE STATISTICS dbo.partsupp WITH FULLSCAN; UPDATE STATISTICS dbo.part WITH FULLSCAN; UPDATE STATISTICS dbo.customer WITH FULLSCAN; GO -
Query code changes (high ROI patterns from plan cache)
Priority 10
- Rewrite IN (SELECT ... GROUP BY ... HAVING ...) to a derived-table join to improve optimization and reduce redundant scans; use explicit JOIN syntax (avoid old-style comma joins).
- Rewrite correlated EXISTS/NOT EXISTS in the supplier “numwait” query to pre-aggregate per order to reduce repeated probes.
/* Pattern 1: Replace IN-subquery with JOIN to aggregated lineitem */ SELECT TOP (100) c.c_name, c.c_custkey, o.o_orderkey, o.o_orderdate, o.o_totalprice, SUM(l.l_quantity) AS sum_qty FROM dbo.customer AS c JOIN dbo.orders AS o ON o.o_custkey = c.c_custkey JOIN dbo.lineitem AS l ON l.l_orderkey = o.o_orderkey JOIN ( SELECT l_orderkey FROM dbo.lineitem GROUP BY l_orderkey HAVING SUM(l_quantity) > 314 ) AS x ON x.l_orderkey = o.o_orderkey GROUP BY c.c_name, c.c_custkey, o.o_orderkey, o.o_orderdate, o.o_totalprice ORDER BY o.o_totalprice DESC, o.o_orderdate OPTION (MAXDOP 4); /* Pattern 2: Reduce repeated EXISTS probes by summarizing per order */ WITH order_supp AS ( SELECT l_orderkey, l_suppkey, MAX(CASE WHEN l_receiptdate > l_commitdate THEN 1 ELSE 0 END) AS has_late FROM dbo.lineitem GROUP BY l_orderkey, l_suppkey ), order_late_stats AS ( SELECT l_orderkey, SUM(has_late) AS late_supp_cnt, COUNT(*) AS supp_cnt FROM order_supp GROUP BY l_orderkey ) SELECT TOP (100) s.s_name, COUNT_BIG(*) AS numwait FROM dbo.orders AS o JOIN dbo.lineitem AS l1 ON l1.l_orderkey = o.o_orderkey JOIN dbo.supplier AS s ON s.s_suppkey = l1.l_suppkey JOIN dbo.nation AS n ON n.n_nationkey = s.s_nationkey JOIN order_late_stats AS z ON z.l_orderkey = o.o_orderkey WHERE o.o_orderstatus = 'F' AND n.n_name = 'PERU' AND l1.l_receiptdate > l1.l_commitdate AND z.supp_cnt > 1 AND z.late_supp_cnt = 1 GROUP BY s.s_name ORDER BY numwait DESC, s.s_name OPTION (MAXDOP 4);These rewrites align with the recommended indexes ondbo.lineitem (l_orderkey, l_suppkey)anddbo.ordersdate/customer keys and typically reduce logical reads by avoiding repeated correlated probes.
Deployment script (ordered, safe, online where possible)
- Includes: PK conversion for orders (with FK drop/recreate), index creates, optional NCCI on orders, stats updates.
- Compression: PAGE for large analytic tables; Fillfactor 98 (mostly read-heavy, low fragmentation expectation).
USE [tpch];
GO
/* =========================
1) dbo.orders: heap fix
========================= */
IF EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = 'lineitem_order_fk')
BEGIN
ALTER TABLE dbo.lineitem DROP CONSTRAINT lineitem_order_fk;
END
GO
IF EXISTS (SELECT 1 FROM sys.key_constraints WHERE name = 'orders_pk' AND parent_object_id = OBJECT_ID('dbo.orders'))
BEGIN
ALTER TABLE dbo.orders DROP CONSTRAINT orders_pk;
END
GO
ALTER TABLE dbo.orders
ADD CONSTRAINT orders_pk
PRIMARY KEY CLUSTERED (o_orderkey)
WITH
(
FILLFACTOR = 98,
DATA_COMPRESSION = PAGE,
ONLINE = ON,
SORT_IN_TEMPDB = ON
);
GO
ALTER TABLE dbo.lineitem
ADD CONSTRAINT lineitem_order_fk
FOREIGN KEY (l_orderkey) REFERENCES dbo.orders(o_orderkey);
GO
/* =========================
2) dbo.lineitem: NCCI
========================= */
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'NCCX_lineitem_analytics' AND object_id = OBJECT_ID('dbo.lineitem'))
BEGIN
CREATE NONCLUSTERED COLUMNSTORE INDEX NCCX_lineitem_analytics
ON dbo.lineitem
(
l_orderkey, l_partkey, l_suppkey,
l_shipdate, l_commitdate, l_receiptdate,
l_returnflag, l_linestatus,
l_quantity, l_extendedprice, l_discount, l_tax,
l_shipmode, l_shipinstruct
)
WITH (ONLINE = ON);
END
GO
/* =========================
3) dbo.lineitem: targeted rowstore NCIs
========================= */
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_lineitem_orderkey_cover' AND object_id = OBJECT_ID('dbo.lineitem'))
BEGIN
CREATE INDEX IX_lineitem_orderkey_cover
ON dbo.lineitem (l_orderkey)
INCLUDE (l_quantity, l_extendedprice, l_discount, l_suppkey, l_partkey, l_commitdate, l_receiptdate, l_returnflag)
WITH (FILLFACTOR = 98, DATA_COMPRESSION = PAGE, ONLINE = ON, SORT_IN_TEMPDB = ON);
END
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_lineitem_order_supp_receipt_commit' AND object_id = OBJECT_ID('dbo.lineitem'))
BEGIN
CREATE INDEX IX_lineitem_order_supp_receipt_commit
ON dbo.lineitem (l_orderkey, l_suppkey)
INCLUDE (l_receiptdate, l_commitdate)
WITH (FILLFACTOR = 98, DATA_COMPRESSION = PAGE, ONLINE = ON, SORT_IN_TEMPDB = ON);
END
GO
/* =========================
4) dbo.orders: supporting rowstore NCIs
========================= */
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_orders_cust_orderdate' AND object_id = OBJECT_ID('dbo.orders'))
BEGIN
CREATE INDEX IX_orders_cust_orderdate
ON dbo.orders (o_custkey, o_orderdate)
INCLUDE (o_orderkey, o_totalprice)
WITH (FILLFACTOR = 98, DATA_COMPRESSION = PAGE, ONLINE = ON, SORT_IN_TEMPDB = ON);
END
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_orders_orderdate' AND object_id = OBJECT_ID('dbo.orders'))
BEGIN
CREATE INDEX IX_orders_orderdate
ON dbo.orders (o_orderdate)
INCLUDE (o_orderkey, o_custkey, o_shippriority, o_orderpriority, o_orderstatus)
WITH (FILLFACTOR = 98, DATA_COMPRESSION = PAGE, ONLINE = ON, SORT_IN_TEMPDB = ON);
END
GO
/* =========================
5) dbo.partsupp: supplier lookup NCI
========================= */
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_partsupp_suppkey' AND object_id = OBJECT_ID('dbo.partsupp'))
BEGIN
CREATE INDEX IX_partsupp_suppkey
ON dbo.partsupp (ps_suppkey)
INCLUDE (ps_supplycost, ps_availqty, ps_partkey)
WITH (FILLFACTOR = 98, DATA_COMPRESSION = PAGE, ONLINE = ON, SORT_IN_TEMPDB = ON);
END
GO
/* =========================
6) Optional: dbo.orders NCCI (comment out if DML-heavy)
========================= */
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'NCCX_orders_analytics' AND object_id = OBJECT_ID('dbo.orders'))
BEGIN
CREATE NONCLUSTERED COLUMNSTORE INDEX NCCX_orders_analytics
ON dbo.orders (o_orderkey, o_custkey, o_orderdate, o_totalprice, o_orderpriority, o_shippriority, o_orderstatus)
WITH (ONLINE = ON);
END
GO
/* =========================
7) Stats
========================= */
ALTER DATABASE [tpch] SET AUTO_UPDATE_STATISTICS_ASYNC ON;
GO
UPDATE STATISTICS dbo.lineitem WITH FULLSCAN;
UPDATE STATISTICS dbo.orders WITH FULLSCAN;
UPDATE STATISTICS dbo.partsupp WITH FULLSCAN;
UPDATE STATISTICS dbo.part WITH FULLSCAN;
UPDATE STATISTICS dbo.customer WITH FULLSCAN;
GO