SQL 对单个 table 值参数的查询在大输入时速度较慢

SQL Query on single table-valued parameter slow on large input

我有一个table,定义很简单:

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)

我需要在 table 中查询与 UserId 列表相关的所有故事,按创建时间排序,然后仅获取 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;

使用这个用户定义的 Table 类型:

CREATE TYPE UserIdInput AS TABLE 
(
    UID nvarchar(100) PRIMARY KEY CLUSTERED
)

table 有 1300 万行,当使用很少的用户 ID 作为输入时,我得到了很好的结果,但当提供数百或几千个用户 ID 作为输入时,结果非常糟糕(30 多秒)。主要问题似乎是它使用了 63% 的工作量在排序上。

我缺少什么索引?这似乎是对单个 table.

的非常直接的查询

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

一些 "better"(但不理想)解决方案是:

  1. 如果确实需要 Unicode 字符,至少要指定二进制排序规则,例如 Latin1_General_100_BIN2.
  2. 如果您不需要 Unicode 字符,则切换到使用 VARCHAR,这将占用 space 的一半,并且排序/比较速度更快。另外,仍然使用二进制排序规则。

你最好的选择是:

  1. INT IDENTITY 列添加到 User table,命名为 UseID
  2. 使 UserID 集群 PK
  3. Related table 中添加一个 INT(没有 IDENTITY)列,命名为 UserID
  4. UserID
  5. 上将 Related 的 FK 添加回 User
  6. Related table 中删除 RelatedUser 列。
  7. UserCode 列的 User table 添加一个非聚集的唯一索引(这使它成为 "alternate key")
  8. 删除并重新创建 UserIdInput 用户定义的 Table 类型以具有 INT 数据类型而不是 NVARCHAR(100)
  9. 如果可能的话,将 User table 的 ID 列更改为二进制排序规则(即 Latin1_General_100_BIN2
  10. 如果可能,将 User table 中的当前 Id 列重命名为 UserCode 或类似名称。
  11. 如果用户输入 "Code" 值(意思是:不能保证他们将始终使用全部大写或全部小写),那么最好添加一个 AFTER INSERT, UPDATE 触发器User table 以确保值始终全部为大写(或全部为小写)。这也意味着您需要确保所有传入查询在搜索 "Code" 时使用相同的全大写或全小写值。但是,一点点额外的工作都会得到回报。

整个系统都会感谢你,并通过提高效率来表达对你的感激:-)。

需要考虑的另一件事: TVP 是一个 table 变量,默认情况下,那些只在查询优化器看来只有一行。因此,在 TVP 中添加几千个条目会减慢它的速度是有道理的。在这种情况下帮助加速 TVP 的一个技巧是将 OPTION (RECOMPILE) 添加到查询中。使用 table 变量重新编译查询将导致查询优化器看到真实的行数。如果这没有任何帮助,另一个技巧是将 TVP table 变量转储到本地临时 table(即 #TempUserIDs),因为它们会维护统计数据并在您拥有时更好地优化其中的行数不多。

来自O.P.对这个回答的评论:

[UID] is an ID used across our system (XXX-Y-ZZZZZZZZZZ...), XXX being letters, Y being a number and Z being numbers

是的,我认为这是某种 ID 或代码,所以这不会改变我的建议。 NVARCHAR,特别是如果使用非二进制、不区分大小写的排序规则,可能是该值最糟糕的数据类型选择之一。此 ID 应位于 User table 中名为 UserCode 的列中,并在其上定义了非聚集索引。这使它成为一个 "alternate" 键和从应用程序层快速轻松地查找一次,以获得该行的 "internal" 整数值,INT IDENTITY 列作为实际 UserID(通常最好将 ID 列命名为 {table_name}ID 以保持一致性/随着时间的推移更易于维护)。 UserID INT 值是进入所有相关 tables 成为 FK 的值。 INT 列加入 NVARCHAR 快得多 。即使使用二进制排序规则,此 NVARCHAR 列虽然比其当前实现更快,但仍将至少为 32 字节(基于 XXX-Y-ZZZZZZZZZZ 的给定示例),而 INT 将只有 4 个字节。是的,那些额外的 28 个字节 do 会有所作为,尤其是当您有 1300 万行时。请记住,这不仅仅是这些值占用的磁盘 space,它也是内存,因为为查询读取的所有数据都经过缓冲池(即物理内存!)。

In this scenario, however, we're not following the foreign keys anywhere, but directly querying on them. If they're indexed, should it matter?

是的,它仍然很重要,因为您实际上是在执行与 JOIN 相同的操作:您正在获取主 table 中的每个值并将其与 table 变量中的值进行比较/ TVP。这仍然是一个非二进制、不区分大小写(我假设)的比较,与二进制比较相比非常慢。每个字母不仅需要根据大写和小写字母进行评估,还需要根据可能等同于每个字母的所有其他 Unicode 代码点进行评估(并且有比您认为匹配 A - Z 的更多!)。索引将使它比没有索引更快,但远不及比较一个没有其他表示的简单值那么快。

The main problem seems to be that it uses 63% of the effort on sorting.

ORDER BY CreationTime DESC

我建议在 CreationTime 上建立索引

或尝试在 RelatedStory、CreationTime 上建立索引

所以我终于找到了解决办法。

虽然 @srutzky 提出了通过将 NVARCHAR UserId 更改为 Integer 以最小化比较成本来规范化表的好建议,但这并不能解决我的问题。为了增加理论性能,我肯定会在某个时候这样做,但我发现在立即实施后性能几乎没有变化。

@Paparazzi 建议我为 (RelatedStory, CreationTime) 添加一个索引,但这也没有满足我的需要。 原因是,我还需要为 RelatedUser 建立索引,因为这就是查询的方式,它按 CreationTime 和 RelatedStory 进行分组和排序,因此这三个都需要。所以:

CREATE INDEX i_idandtime ON Related (RelatedUser, CreationTime DESC, RelatedStory)

解决了我的问题,将我无法接受的 15 秒以上的查询时间减少到大部分 1 秒或几秒的查询时间。

我认为给我启示的是@srutzky 注意到:

Remember, "Include" columns are not used for sorting or comparisons, only for covering.

这让我意识到我需要索引中的所有 groupby 和 orderby 列。

因此,虽然我无法将以上任何一位 post 人 post 标记为答案,但我要衷心感谢他们抽出宝贵时间。