我有一个包含这个简单定义的表:
CREATE TABLE Related
(
RelatedUser NVARCHAR(100) NOT NULL FOREIGN KEY REFERENCES User(Id),
RelatedStory BIGINT NOT NULL FOREIGN KEY REFERENCES Story(Id),
CreationTime DateTime NOT NULL,
PRIMARY KEY(RelatedUser, RelatedStory)
);
使用这些索引:
CREATE INDEX i_relateduserid
ON Related (RelatedUserId) INCLUDE (RelatedStory, CreationTime)
CREATE INDEX i_relatedstory
ON Related(RelatedStory) INCLUDE (RelatedUser, CreationTime)
我需要查询表中所有与UserIds列表相关的故事,按创建时间排序,然后只获取X并跳过Y.
我有这个存储过程:
CREATE PROCEDURE GetStories
@offset INT,
@limit INT,
@input UserIdInput READONLY
AS
BEGIN
SELECT RelatedStory
FROM Related
WHERE EXISTS (SELECT 1 FROM @input WHERE UID = RelatedUser)
GROUP BY RelatedStory, CreationTime
ORDER BY CreationTime DESC
OFFSET @offset ROWS FETCH NEXT @limit ROWS ONLY;
END;
使用此用户定义的表类型:
CREATE TYPE UserIdInput AS TABLE
(
UID nvarchar(100) PRIMARY KEY CLUSTERED
)
该表有1300万行,当使用少量userids作为输入时,我获得了良好的结果,但是当提供数百或数千个用户ID作为输入时,结果非常糟糕(30+秒)。主要问题似乎是它使用了63%的分拣工作。
我错过了什么指数?这似乎是在单个表上非常简单的查询。
答案 0 :(得分:2)
您对RelatedUser
/ UID
有哪些类型的值?究竟,为什么要使用NVARCHAR(100)
呢?对于PK / FK字段,NVARCHAR
通常是一个可怕的选择。即使该值是简单的字母数字代码(例如ABTY1245
),也有更好的方法来处理它。 NVARCHAR
(对于此特定问题甚至使用VARCHAR
)的主要问题之一是,除非您使用二进制排序规则(例如Latin1_General_100_BIN2
),否则每个排序和比较操作都将应用所有语言规则,这在使用字符串时非常值得,但在使用代码时非常昂贵,尤其是,当使用通常默认的不区分大小写的排序规则时。
有些"更好" (但不是理想的)解决方案是:
Latin1_General_100_BIN2
。VARCHAR
,这将占用一半的空间并更快地排序/比较。此外,仍然使用二进制排序。你最好的选择是:
INT IDENTITY
列添加到User
表,名为UseID
UserID
群集PK INT
(无IDENTITY
)列添加到Related
表,名为UserID
Related
User
的FK添加回UserID
RelatedUser
表中删除Related
列。User
列上的UserCode
表格中(这使其成为"备用密钥")UserIdInput
用户定义的表类型,使其具有INT
数据类型,而不是NVARCHAR(100)
ID
表的User
列以进行二进制整理(即Latin1_General_100_BIN2
)Id
表格中的当前User
列重命名为UserCode
或类似名称。AFTER INSERT, UPDATE
表上添加User
触发器,以确保值总是高于...案例(或所有小写)。这也意味着在搜索" Code"时,您需要确保所有传入的查询使用相同的全大写或全部小写值。但是,这一点额外的工作将会得到回报。整个系统都会感谢你,并通过提高效率向你表示赞赏: - )。
要考虑的另一件事: TVP是一个表变量,默认情况下,查询优化器只会出现一行。因此,在TVP中添加几千个条目会减慢它的速度。在这种情况下,帮助加速TVP的一个技巧是向查询添加OPTION (RECOMPILE)
。使用表变量重新编译查询将使查询优化器查看真正的行计数。如果这对任何事都没有帮助,那么另一个技巧就是将TVP表变量转储到本地临时表(即#TempUserIDs
)中,因为那些维护统计数据并在你有少量数据时更好地优化他们中的行。
来自O.P。对这个答案的评论:
[UID]是我们系统中使用的ID(XXX-Y-ZZZZZZZZZZ ...),XXX是字母,Y是数字,Z是数字
是的,我认为这是某种ID或代码,因此不会改变我的建议。 NVARCHAR
,特别是如果使用非二进制,不区分大小写的排序规则,可能是此值的最差数据类型选择之一。此ID应位于UserCode
表中名为User
的列中,并在其上定义非聚集索引。这使它成为一个"替代"密钥和从应用层快速简单的查找,一次,以获得"内部"该行的整数值,INT IDENTITY
列为实际UserID
(通常最好将ID列命名为{table_name} ID,以保持一致性/随着时间的推移更容易维护)。 UserID
INT值是所有相关表中的FK。 INT
列比NVARCHAR
更快地加入 。即使使用二进制排序规则,此NVARCHAR
列虽然比其当前实现速度快,但仍至少为32个字节(基于XXX-Y-ZZZZZZZZZZ
的给定示例),而INT
将只需4个字节。是的,那些额外的28个字节做会有所不同,特别是当你有1300万行时。请记住,这不仅仅是这些值占用的磁盘空间,它也是内存,因为为查询读取的所有数据都通过缓冲池(即物理内存!)。
但是,在这种情况下,我们不会在任何地方跟踪外键,而是直接查询它们。如果他们被编入索引,是否重要?
是的,它仍然很重要,因为您基本上执行与JOIN相同的操作:您将获取主表中的每个值并将其与表变量/ TVP中的值进行比较。这仍然是一个非二进制,不区分大小写(我假设)的比较,与二进制比较相比非常慢。每个字母不仅需要针对大写和小写进行评估,还要针对可能等同于每个字母的所有其他Unicode代码点进行评估(并且有超过您认为的匹配A - Z
!)。索引会比没有索引更快,但远不及比较一个没有其他表示的简单值。
答案 1 :(得分:2)
所以我终于找到了解决方案。
虽然@srutzky通过将NVARCHAR UserId更改为Integer以最小化比较成本来对表进行规范化提出了很好的建议,但这并不能解决我的问题。我肯定会在某些方面做到这一点,以增加理论性能,但是在实施它之后我看到的性能变化很小。
@Paparazzi建议我为(RelatedStory,CreationTime)添加一个索引,但这并没有做我需要的。 原因是,我还需要索引RelatedUser,因为这是查询的方式,它由CreationTime和RelatedStory分组和排序,所以这三个都是必需的。所以:
CREATE INDEX i_idandtime ON Related (RelatedUser, CreationTime DESC, RelatedStory)
解决了我的问题,将我不可接受的15秒以上的查询时间缩短到大多数1秒或几秒的查询时间。
我认为给我启示的是@srutzky注意到:
请记住,“包含”列不用于排序或比较, 仅用于覆盖。
让我意识到我需要索引中的所有groupby和orderby列。
因此,虽然我不能将上述任何一张海报标记为答案,但我要真诚地感谢他们的时间。
答案 2 :(得分:1)
主要问题似乎是它使用了63%的努力 排序
ORDER BY CreationTime DESC
我建议并在CreationTime上编制索引
或者在RelatedStory上创建一个索引,CreationTime