单个表值参数的SQL查询在大输入上缓慢

时间:2016-10-03 19:07:19

标签: sql-server performance tsql table-valued-parameters

我有一个包含这个简单定义的表:

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%的分拣工作。

我错过了什么指数?这似乎是在单个表上非常简单的查询。

3 个答案:

答案 0 :(得分:2)

您对RelatedUser / UID有哪些类型的值?究竟,为什么要使用NVARCHAR(100)呢?对于PK / FK字段,NVARCHAR通常是一个可怕的选择。即使该值是简单的字母数字代码(例如ABTY1245),也有更好的方法来处理它。 NVARCHAR(对于此特定问题甚至使用VARCHAR)的主要问题之一是,除非您使用二进制排序规则(例如Latin1_General_100_BIN2),否则每个排序和比较操作都将应用所有语言规则,这在使用字符串时非常值得,但在使用代码时非常昂贵,尤其是,当使用通常默认的不区分大小写的排序规则时。

有些"更好" (但不是理想的)解决方案是:

  1. 如果您确实需要Unicode字符,请至少指定二进制排序规则,例如Latin1_General_100_BIN2
  2. 如果您不需要Unicode字符,请切换到使用VARCHAR,这将占用一半的空间并更快地排序/比较。此外,仍然使用二进制排序。
  3. 你最好的选择是:

    1. INT IDENTITY列添加到User表,名为UseID
    2. 使UserID群集PK
    3. INT(无IDENTITY)列添加到Related表,名为UserID
    4. Related
    5. 上将User的FK添加回UserID
    6. RelatedUser表中删除Related列。
    7. 将非群集的唯一索引添加到User列上的UserCode表格中(这使其成为"备用密钥")
    8. 删除并重新创建UserIdInput用户定义的表类型,使其具有INT数据类型,而不是NVARCHAR(100)
    9. 如果可能,请更改ID表的User列以进行二进制整理(即Latin1_General_100_BIN2
    10. 如果可能,请将Id表格中的当前User列重命名为UserCode或类似名称。
    11. 如果用户正在输入" Code"值(意思是:不能保证它们总是使用所有大写或全部小写),然后最好在AFTER INSERT, UPDATE表上添加User触发器,以确保值总是高于...案例(或所有小写)。这也意味着在搜索" Code"时,您需要确保所有传入的查询使用相同的全大写或全部小写值。但是,这一点额外的工作将会得到回报。
    12. 整个系统都会感谢你,并通过提高效率向你表示赞赏: - )。

      要考虑的另一件事: 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