Supposing I have this table (in a multi-tenant SaaS database:
CREATE TABLE dbo.Messages (
TenantId int NOT NULL,
RecipientId int NOT NULL,
MessageId int NOT NULL IDENTITY,
SentUtc datetime2(7) NOT NULL,
IsDeleted bit NOT NULL, /* Less than 5% of rows have IsDeleted = 1 */
-- Body, Subject, etc
CONSTRAINT PK_Messages PRIMARY KEY ( MessageId, TenantId ),
CONSTRAINT UK_Messages UNIQUE ( MessageId ),
CONSTRAINT FK_Messages_Tenants FOREIGN KEY ( TenantId ) REFERENCES dbo.Tenants ( TenantId ),
CONSTRAINT FK_Messages_Recipients FOREIGN KEY ( RecipientId, TenantId ) REFERENCES dbo.Recipients ( RecipientId, TenantId )
);
I have a VIEW
that is meant to get the latest MessageId
for each RecipientId
:
CREATE VIEW dbo.LatestMessages
WITH SCHEMABINDING
AS
WITH latestMessageUtcPerRecipient AS (
SELECT
m.TenantId,
m.RecipientId,
MAX( m.SentUtc ) AS MaxSentUtc
FROM
dbo.Messages AS m
WHERE
m.IsDeleted = 0
GROUP BY
m.TenantId,
m.RecipientId
)
SELECT
lm.TenantId,
lm.RecipientId,
m .MessageId,
m .SentUtc
FROM
dbo.Messages AS m
INNER JOIN latestMessageUtcPerRecipient AS lm ON
m.TenantId = lm.TenantId
AND
m.RecipientId = lm.RecipientId
AND
m.SentUtc = lm.MaxSentUtc
I currently have foreign-key-covering (non-clustered) indexes over
FX_Tenants ( TenantId )
andFX_Recipients ( RecipientId, TenantId )
.Without any other extra
INDEX
objects ondbo.Messages
, thisVIEW
's execution-plan currently performs an Index Scan on theFX_Recipients
index where it reads 14,000% of the final query rows (erk!)So I need to create a good index for this
VIEW
's query that covers theTenantId
,RecipientId
, andSentUtc
- and I understand I should excludeIsDeleted
from the index as is a low-selectivity column) - the problem is I'm unsure what order the columns should go in because I don't know ifSentUtc
should go first (to aid theMAX( m.SentUtc )
aggregate) - or ifRecipientId
should go first (as it's the most selective column for theGROUP BY
clause).So I first tried this index:
CREATE INDEX IX_for_LatestMessages_VIEW ( LatestUtc, RecipientId, TenantId )
...but after creating that, and running
SELECT TOP 1000 * FROM dbo.LatestMessages
, the execution-plan is actually worse than when I didn't have this index (and theFX_Recipients
index now has a 28,700% over-read index-scan which is far worse than the ~14,000% from earlier).Then I tried:
CREATE INDEX IX_for_LatestMessages_VIEW ( RecipientId, TenantId, LatestUtc )
...and to my surprise this resulted in a much better plan, with zero reads from
FX_Recipients
. Though SSMS's "Missing index details" area appears and it really wants me to create another index asON dbo.Messages ( IsDeleted ) INCLUDE ( RecipientId, LatestUtc )
- but I'm wary of doing what it suggests becauseIsDeleted
is not a very selective column (I'm fairly confident theSTATISTICS
are up-to-date too).
Is there a good, single INDEX
definition for this VIEW
that avoids the index-scans of my first attempt while also not setting-off SQL Server's missing-index alarms?
Also, because the VIEW
only gets MessageId
from dbo.Messages
I was hoping that adding INCLUDE ( MessageId )
would eliminate a Key Lookup step in the execution-plan, but it didn't seem to have any effect (presumably because MessageId
is already in the table's composite PK?) - in which case why is the plan doing a Key Lookup (with a 7,700% over-read) in addition to the Index Scan over IX_for_LatestMessages_VIEW
?