Index Tuning Report – RockyPC – 2026-09-22 09:24:55 UTC
Tuning goal: Index Tuning
Server: RockyPC | Database: PhotoCleanup | Version: SQL Server 2022 RTM-GDR 16.0.1200.5 | Edition: Developer Edition, Enterprise engine capabilities
Executive summary
Top priority: reduce full-table work on dbo.Files
Query Store shows repeated or extremely expensive scans of dbo.Files, including a 19,955-second workload entry returning 537,863 rows, a 13,506-second entry returning 345,737 rows, and multiple aggregate queries with approximately 43,000–48,000 logical reads per execution. Create narrowly targeted filtered indexes for live, non-placeholder images awaiting perceptual hashing, live videos awaiting fingerprinting, and live-file aggregates.
Repair severe physical index degradation
Several heavily used indexes have page density below 75% and fragmentation above 80%. Rebuild the clustered PK_Files and the operationally important IX_Files_RootId_PathHash, IX_Files_ScanState_IsQuarantined, and IX_Files_SizeBytes using online PAGE-compressed rebuilds with a controlled fill factor.
Remove demonstrably unused nonconstraint indexes
Drop five indexes with zero seeks and zero or negligible scans after replacement indexes are created. Their combined reported storage is approximately 35.0 MB, and they add write and maintenance overhead without demonstrated read benefit.
Enable automatic last-good-plan correction
FORCE_LAST_GOOD_PLAN is desired as DEFAULT but is actually OFF. Enable it so SQL Server can automatically revert regressions. No forced-plan failures were reported, so there is no immediate unforce operation.
| Overall finding | Assessment | Confidence |
|---|---|---|
| Missing-index DMV report | No direct missing-index recommendations; workload evidence still supports selective filtered indexes. | High |
| Index bloat and fragmentation | Severe on several dbo.Files indexes; maintenance is justified for high-use structures. | High |
| Duplicate/overlap risk | No duplicate or left-prefix overlaps detected in the supplied scope. | High |
| Write overhead | dbo.Files is write-active, especially during bulk synchronization; avoid broad low-cardinality indexes. | High |
| Columnstore suitability | No table over 1,000,000 rows was reported. Do not add columnstore indexes based on this data. | High |
Environment and evidence
- SQL Server 2022, compatibility and feature set capable of online operations, compression, columnstore, PSP optimization, and automatic plan correction.
- The largest reported table is
dbo.Fileswith 567,884 rows; it is large enough for selective rowstore indexes but below the supplied columnstore threshold. - Automatic create and update statistics are enabled; asynchronous statistics updates are disabled.
- All supplied foreign keys and check constraints are trusted and enabled. No constraint repair script is required.
- No deadlock, blocking-chain, Query Store forced-plan failure, disabled index, heap, or duplicate-index evidence was supplied.
Detailed prioritized recommendations
-
1. Create three selective filtered indexes on dbo.Files
Recommendation: create:
IX_Files_LiveImage_PHashPendingon(PHash), filtered toIsImage = 1 AND IsMissing = 0 AND IsQuarantined = 0 AND IsPlaceholder = 0 AND PHash IS NULL, includingFileId, RootId, RelativePath, SizeBytes, Extension.IX_Files_LiveVideo_FingerprintPendingon(VideoFingerprint), filtered toIsVideo = 1 AND IsMissing = 0 AND IsQuarantined = 0 AND IsPlaceholder = 0 AND VideoFingerprint IS NULL, includingFileId, RootId, RelativePath, SizeBytes, Extension.IX_Files_Live_SizeByteson(SizeBytes), filtered toIsMissing = 0, includingFileId, IsPlaceholder, IsImage, IsVideo, IsQuarantined, ScanState.
Evidence: Query Store reports 623 seconds for the PHash-pending query with 17,367 actual rows and 41,814 reads, 199 seconds for the video-fingerprint query with 235 rows and 4,801 reads, and repeated live-file count/sum queries with approximately 43,716 reads per execution. The supplied missing-index overlap data also reports a 43.3 MB candidate matching the PHash workload and a 13.0 MB candidate for
IsMissingwithSizeBytes.Estimated size: PHash index 43.3 MB using the supplied candidate estimate for 567,884 rows; video index 20–45 MB because no candidate size or complete column widths were supplied; live aggregate index 13.0 MB using the supplied candidate estimate. Assumptions are filtered-row counts comparable to the workload, existing clustering key propagation, rowstore PAGE compression, and normal page overhead.
Net storage impact: approximately 76.3–101.3 MB added before drops and rebuild effects.
Risk: Medium. Every insert or update affecting filter columns can maintain these indexes. The filters are selective and the workload evidence is strong.
Confidence: High for the PHash and live aggregate indexes; Medium-High for the video index.
-
2. Rebuild heavily fragmented, high-value indexes
Rebuild
PK_Files,IX_Files_RootId_PathHash,IX_Files_ScanState_IsQuarantined, andIX_Files_SizeBytesonline with PAGE compression and fill factor 95. These structures have reported page densities of 60.43%, 51.66%, 43.66%, and 60.31%, respectively, with fragmentation from 82.17% to 98.90%.Index Rows / before size After estimate Net change Rationale PK_Files570,600 / 329.5 MB 280–360 MB -49.5 to +30.5 MB High seeks and 99% fragmentation; clustered access path for all dependent indexes. IX_Files_RootId_PathHash567,884 / 72.1 MB 55–75 MB -17.1 to +2.9 MB Critical synchronization join key; used by INSERT and UPDATE workloads. IX_Files_ScanState_IsQuarantined567,884 / 37.6 MB 28–40 MB -9.6 to +2.4 MB 14 seeks and 12 scans; supports scan-state processing. IX_Files_SizeBytes567,884 / 37.3 MB 28–40 MB -9.3 to +2.7 MB Used by size-based duplicate processing and has high fragmentation. Estimates assume SQL Server PAGE compression and fill factor 95; actual size depends on data compressibility and row distribution. Online rebuilds still consume CPU, I/O, log space, and temporary sort space.
Risk: Medium; schedule outside the busiest synchronization window and verify transaction-log capacity.
Confidence: High for
IX_Files_RootId_PathHash; Medium-High for the other three. -
3. Drop unused nonconstraint indexes after filtered-index creation
Drop the following indexes:
dbo.Files.IX_Files_IsImage_IsQuarantined: 0 seeks, 0 scans, 17.6 MB.dbo.Files.IX_Files_QuickHash: 0 seeks, 0 scans, 6.2 MB.dbo.Actions.IX_Actions_FileId_ActionType: 0 seeks, 0 scans, 1.6 MB.dbo.DupMembers.IX_DupMembers_FileId: 0 seeks, 0 scans, 5.9 MB.dbo.DupGroups.IX_DupGroups_WastedBytes: 0 seeks, 2 scans, 3.7 MB and 683 updates.
These are not reported as primary-key or unique-constraint backing indexes. Retain all constraint-enforcing indexes. DMV counters reset after restart, detach, restore, or index recreation; the recommendation is therefore appropriate only for the supplied observation period.
Storage reclaimed: approximately 35.0 MB.
Risk: Medium, mainly because the supplied observation window is not explicitly stated. Confidence: Medium-High.
-
4. Enable FORCE_LAST_GOOD_PLAN
The desired state is DEFAULT while the actual state is OFF, with reason AUTO_CONFIGURED. Enable the database-scoped option. This is a safe corrective action for a workload showing large, scan-heavy executions, even though no forced-plan failures were reported.
Risk: Low. Automatic tuning may choose a prior plan that is safer but not optimal for every parameter distribution; monitor plan selection after enabling.
Confidence: High.
-
5. Improve synchronization query design and staging indexes
The most expensive write statements join
dbo.Filesto temporary tables usingRootId, PathHash. The existingIX_Files_RootId_PathHashis the correct access path and should be retained and rebuilt. Ensure#Stagingand#HashUpdatereceive temporary indexes on their join keys before the UPDATE or INSERT statements. This is a query-code and temporary-table change rather than a permanent database index recommendation.The large Query Store entries show 3,465 seconds for the hash-update statement and 3,305 seconds for a synchronization UPDATE. Because the staging-table definitions were not supplied, no executable permanent-table script is generated for them.
Risk: Low for adding correctly scoped temporary indexes; confidence: Medium.
Index usage assessment
| Table | Useful access paths | Write/read interpretation |
|---|---|---|
dbo.Files | PK_Files: 2,362 seeks, 99 scans; IX_Files_ScanState_IsQuarantined: 14 seeks, 12 scans; IX_Files_SizeBytes: 1 seek, 6 scans; IX_Files_RootId_PathHash supports synchronization despite low read counters. | High-value table with 567,884 rows and heavy bulk updates. Add only filtered or join-specific indexes. |
dbo.DupGroups | PK_DupGroups: 1,716 seeks, 175 scans; IX_DupGroups_GroupType_ResolvedUtc: 191 seeks, 12 scans. | Existing composite index aligns with frequent GroupType/ResolvedUtc predicates. Do not add another index. |
dbo.DupMembers | PK_DupMembers: 83 seeks, 15 scans. | Composite primary key supports GroupId/FileId joins. The unused FileId-only index can be removed cautiously. |
dbo.Actions | PK_Actions: scans and lookups; IX_Actions_PerformedUtc has scans. | No new index justified. ActionType has one distinct value and is unsuitable as a standalone key. |
Low-cardinality columns such as IsImage, IsVideo, IsPlaceholder, and IsQuarantined should not receive broad standalone indexes. Their use inside highly selective filtered predicates is justified by the Query Store workload.
Page density and maintenance
Indexes below approximately 75% page utilization include 12 structures. The highest-impact cases are the dbo.Files indexes selected for rebuild. Fragmentation is above 80% on most of them, indicating wasted pages and likely page-split or update pressure. The supplied latch waits are modest, so the primary expected benefit is reduced I/O and improved scan efficiency rather than latch-wait elimination.
| Action | Indexes | Reason |
|---|---|---|
| Rebuild now | PK_Files, IX_Files_RootId_PathHash, IX_Files_ScanState_IsQuarantined, IX_Files_SizeBytes | High usage or synchronization importance, density 43.66–60.43%, fragmentation 82.17–98.90%. |
| Drop instead of rebuild | IX_Files_IsImage_IsQuarantined, IX_Files_QuickHash, IX_Actions_FileId_ActionType, IX_DupMembers_FileId, IX_DupGroups_WastedBytes | Unused or nearly unused; maintenance cost exceeds demonstrated read value. |
| Monitor | IX_Files_Sha256, IX_Files_NameKey, IX_Actions_PerformedUtc | Some scans or workload relevance exist; no immediate change. |
Query Store workload insights
The Query Store telemetry contains several suspiciously large duration values and inconsistent execution-count formatting, so absolute totals should be validated against runtime statistics before declaring a sustained outage. Nevertheless, the relative pattern is clear: dbo.Files scans and synchronization operations dominate the workload.
| Workload example | Observed metrics | Index implication |
|---|---|---|
| PHash pending image query | 1 execution, 623 s, 528,884 ms duration, 41814 reads, 17,367 rows. | Strong candidate for IX_Files_LiveImage_PHashPending. |
| Video fingerprint pending query | 1 execution, 199 s, 198,786 ms duration, 4801 reads, 235 rows. | Strong candidate for IX_Files_LiveVideo_FingerprintPending. |
| Files synchronization UPDATE | 1 execution, 3,465 s, 1,682,212 reads, 69,216 writes, 520,299 rows. | Retain and rebuild IX_Files_RootId_PathHash; optimize staging joins. |
| Live-file count and sum aggregates | 10–11 executions each; approximately 128–174 seconds average and 43,716 reads. | Use filtered live-file index with SizeBytes. |
| DupGroups aggregate | Repeated GroupType and ResolvedUtc predicates; plan-cache reads 389–6,596. | Existing IX_DupGroups_GroupType_ResolvedUtc is correctly aligned; no new index. |
No forced-plan failures were detected. Therefore there are no affected query_id or plan_id values, failure counts, timestamps, or failure reasons to classify. No unforce or plan recreation is warranted.
Automatic tuning
| Option | Desired state | Actual state | Finding |
|---|---|---|---|
FORCE_LAST_GOOD_PLAN | DEFAULT | OFF | Actual state differs from the desired operational posture. Enable explicitly. |
Because Automatic Plan Correction is currently not enabled, the immediate action is to enable it rather than manually force or unforce any plan. After enabling, allow the engine to monitor and revert regressions while the index changes are validated.
One-line remediation: Enable last-good-plan correction, add the three selective filtered indexes, rebuild the four important fragmented indexes, then remove only the five unused nonconstraint indexes.
Constraints and statistics
- All supplied foreign keys and check constraints are trusted and enabled. No
ALTER TABLE ... WITH CHECK CHECK CONSTRAINTaction is required. - Trusted constraints allow optimizer join elimination and predicate simplification. Do not disable them during index maintenance.
- Primary keys and their foreign-key relationships must remain intact. None of the recommended changes drops or recreates a primary key, so no foreign-key drop/recreate sequence is required.
- Automatic statistics creation and updates are enabled. Index creation and rebuilds create or refresh relevant index statistics. The implementation script also updates
dbo.Filesstatistics with FULLSCAN after structural changes. - Do not add a columnstore index: no table over 1,000,000 rows was reported. A columnstore index would add storage and write-maintenance overhead without sufficient evidence of an analytic scan workload.
Storage impact summary
| Category | Estimated change |
|---|---|
| New filtered indexes | +76.3 to +101.3 MB |
| Dropped unused indexes | -35.0 MB |
| Rebuilds | -85.5 to +38.5 MB, based on reported before sizes and PAGE-compression estimates |
| Estimated net change | +55.8 to +104.8 MB |
The net range excludes transient sort and transaction-log space. Rebuild estimates are uncertain because actual compression ratios and row widths were not supplied. The new video index is also a range because no source candidate size was reported.
Scripts
Create filtered indexes for high-cost dbo.Files workloads
USE [PhotoCleanup];
GO
IF NOT EXISTS
(
SELECT 1
FROM sys.indexes
WHERE object_id = OBJECT_ID(N'dbo.Files')
AND name = N'IX_Files_LiveImage_PHashPending'
)
BEGIN
CREATE NONCLUSTERED INDEX [IX_Files_LiveImage_PHashPending]
ON [dbo].[Files] ([PHash])
INCLUDE ([FileId], [RootId], [RelativePath], [SizeBytes], [Extension])
WHERE [IsImage] = 1
AND [IsMissing] = 0
AND [IsQuarantined] = 0
AND [IsPlaceholder] = 0
AND [PHash] IS NULL
WITH
(
DATA_COMPRESSION = PAGE,
SORT_IN_TEMPDB = ON,
ONLINE = ON,
FILLFACTOR = 95
);
END;
GO
IF NOT EXISTS
(
SELECT 1
FROM sys.indexes
WHERE object_id = OBJECT_ID(N'dbo.Files')
AND name = N'IX_Files_LiveVideo_FingerprintPending'
)
BEGIN
CREATE NONCLUSTERED INDEX [IX_Files_LiveVideo_FingerprintPending]
ON [dbo].[Files] ([VideoFingerprint])
INCLUDE ([FileId], [RootId], [RelativePath], [SizeBytes], [Extension])
WHERE [IsVideo] = 1
AND [IsMissing] = 0
AND [IsQuarantined] = 0
AND [IsPlaceholder] = 0
AND [VideoFingerprint] IS NULL
WITH
(
DATA_COMPRESSION = PAGE,
SORT_IN_TEMPDB = ON,
ONLINE = ON,
FILLFACTOR = 95
);
END;
GO
IF NOT EXISTS
(
SELECT 1
FROM sys.indexes
WHERE object_id = OBJECT_ID(N'dbo.Files')
AND name = N'IX_Files_Live_SizeBytes'
)
BEGIN
CREATE NONCLUSTERED INDEX [IX_Files_Live_SizeBytes]
ON [dbo].[Files] ([SizeBytes])
INCLUDE ([FileId], [IsPlaceholder], [IsImage], [IsVideo], [IsQuarantined], [ScanState])
WHERE [IsMissing] = 0
WITH
(
DATA_COMPRESSION = PAGE,
SORT_IN_TEMPDB = ON,
ONLINE = ON,
FILLFACTOR = 95
);
END;
GO
Rebuild fragmented dbo.Files indexes with online PAGE compression
USE [PhotoCleanup];
GO
IF EXISTS
(
SELECT 1
FROM sys.indexes
WHERE object_id = OBJECT_ID(N'dbo.Files')
AND name = N'PK_Files'
)
BEGIN
ALTER INDEX [PK_Files] ON [dbo].[Files]
REBUILD WITH
(
ONLINE = ON,
SORT_IN_TEMPDB = ON,
DATA_COMPRESSION = PAGE,
FILLFACTOR = 95
);
END;
GO
IF EXISTS
(
SELECT 1
FROM sys.indexes
WHERE object_id = OBJECT_ID(N'dbo.Files')
AND name = N'IX_Files_RootId_PathHash'
)
BEGIN
ALTER INDEX [IX_Files_RootId_PathHash] ON [dbo].[Files]
REBUILD WITH
(
ONLINE = ON,
SORT_IN_TEMPDB = ON,
DATA_COMPRESSION = PAGE,
FILLFACTOR = 95
);
END;
GO
IF EXISTS
(
SELECT 1
FROM sys.indexes
WHERE object_id = OBJECT_ID(N'dbo.Files')
AND name = N'IX_Files_ScanState_IsQuarantined'
)
BEGIN
ALTER INDEX [IX_Files_ScanState_IsQuarantined] ON [dbo].[Files]
REBUILD WITH
(
ONLINE = ON,
SORT_IN_TEMPDB = ON,
DATA_COMPRESSION = PAGE,
FILLFACTOR = 95
);
END;
GO
IF EXISTS
(
SELECT 1
FROM sys.indexes
WHERE object_id = OBJECT_ID(N'dbo.Files')
AND name = N'IX_Files_SizeBytes'
)
BEGIN
ALTER INDEX [IX_Files_SizeBytes] ON [dbo].[Files]
REBUILD WITH
(
ONLINE = ON,
SORT_IN_TEMPDB = ON,
DATA_COMPRESSION = PAGE,
FILLFACTOR = 95
);
END;
GO
UPDATE STATISTICS [dbo].[Files] WITH FULLSCAN;
GO
Drop unused nonconstraint indexes after replacement creation
USE [PhotoCleanup];
GO
DROP INDEX IF EXISTS [IX_Files_IsImage_IsQuarantined] ON [dbo].[Files];
GO
DROP INDEX IF EXISTS [IX_Files_QuickHash] ON [dbo].[Files];
GO
DROP INDEX IF EXISTS [IX_Actions_FileId_ActionType] ON [dbo].[Actions];
GO
DROP INDEX IF EXISTS [IX_DupMembers_FileId] ON [dbo].[DupMembers];
GO
DROP INDEX IF EXISTS [IX_DupGroups_WastedBytes] ON [dbo].[DupGroups];
GO
Enable automatic last-good-plan correction
USE [PhotoCleanup];
GO
ALTER DATABASE SCOPED CONFIGURATION
SET AUTOMATIC_TUNING (FORCE_LAST_GOOD_PLAN = ON);
GO
Supporting scripts (not part of the change)
Measure index usage, physical density, and compression after implementation
USE [PhotoCleanup];
GO
SELECT
OBJECT_SCHEMA_NAME(i.object_id) AS SchemaName,
OBJECT_NAME(i.object_id) AS TableName,
i.name AS IndexName,
i.type_desc,
i.is_disabled,
i.fill_factor,
ds.user_seeks,
ds.user_scans,
ds.user_lookups,
ds.user_updates,
ips.avg_page_space_used_in_percent,
ips.avg_fragmentation_in_percent,
ips.page_count,
ips.record_count,
p.data_compression_desc
FROM sys.indexes AS i
LEFT JOIN sys.dm_db_index_usage_stats AS ds
ON ds.database_id = DB_ID()
AND ds.object_id = i.object_id
AND ds.index_id = i.index_id
CROSS APPLY sys.dm_db_index_physical_stats
(
DB_ID(), i.object_id, i.index_id, NULL, 'SAMPLED'
) AS ips
LEFT JOIN sys.partitions AS p
ON p.object_id = i.object_id
AND p.index_id = i.index_id
AND p.partition_number = ips.partition_number
WHERE i.object_id IN
(
OBJECT_ID(N'dbo.Files'),
OBJECT_ID(N'dbo.Actions'),
OBJECT_ID(N'dbo.DupGroups'),
OBJECT_ID(N'dbo.DupMembers')
)
AND i.name IS NOT NULL
AND ips.alloc_unit_type_desc = N'IN_ROW_DATA'
ORDER BY ips.avg_page_space_used_in_percent ASC, ips.avg_fragmentation_in_percent DESC;
GO
Check automatic tuning state and Query Store forced-plan failures
USE [PhotoCleanup];
GO
SELECT
name,
value,
value_for_secondary
FROM sys.database_scoped_configurations
WHERE name = N'AUTOMATIC_TUNING';
SELECT
qpf.query_id,
qpf.plan_id,
qpf.failure_count,
qpf.last_force_failure_time,
qpf.last_force_failure_reason_desc
FROM sys.query_store_plan AS qsp
INNER JOIN sys.query_store_query AS q
ON q.query_id = qsp.query_id
INNER JOIN sys.query_store_plan_forcing_policy AS qpf
ON qpf.plan_id = qsp.plan_id
WHERE qpf.failure_count > 0
ORDER BY qpf.last_force_failure_time DESC;
GO
Validation and rollback criteria
Three-step safe implementation playbook
- During a maintenance window, create the three filtered indexes. Confirm creation succeeds and that log, tempdb, CPU, and I/O remain within limits.
- Rebuild the four selected dbo.Files indexes online, then update dbo.Files statistics with FULLSCAN.
- After a stable observation period, drop the five unused indexes and enable FORCE_LAST_GOOD_PLAN. Recheck Query Store and index usage counters.
Rollback criteria: stop or revert the change if synchronization duration increases by more than 20%, writes or log generation materially exceed the maintenance budget, blocking appears, or any critical query regresses by more than 20% at p95 for two consecutive observation intervals. If a new index is unused and adds measurable write latency, remove that specific index. If compression increases CPU materially without reducing I/O, rebuild the affected index using the prior compression setting.
- Confirm the filtered indexes are enabled and have nonzero seeks or scans for their target queries.
- Confirm page density improves substantially and fragmentation is reduced after rebuild.
- Compare Query Store duration, CPU, logical reads, writes, execution count, and plan count before and after.
- Verify synchronization INSERT and UPDATE statements still use the
RootId, PathHashaccess path. - Confirm no foreign key or check constraint becomes untrusted or disabled.
- Confirm automatic tuning reports
FORCE_LAST_GOOD_PLANas ON. - Reassess dropped indexes only after a full representative workload period, since usage counters can reset.