索引这个非常大的表的最佳方法

nia*_*her 4 indexing sql-server sql-server-2008

我有下表

CREATE TABLE DiaryEntries
(
 [userId] [uniqueidentifier] NOT NULL,
 [setOn] [datetime] NOT NULL, -- always set to GETDATE().
 [entry] [nvarchar](255) NULL
)
Run Code Online (Sandbox Code Playgroud)

每个用户每天将插入大约 3 个条目。将有大约 1'000'000 名用户。这意味着该表中每天有 3'000'000 条新记录。一旦记录超过 1 个月,我们就会将其删除。

大多数查询都有以下 WHERE 子句:

WHERE userId = @userId AND setOn > @setOn
Run Code Online (Sandbox Code Playgroud)

大多数查询返回不超过 3 行,除了一个返回本月内插入的所有行(最多 90 行)。

插入记录后,日期和用户 ID 不能更改。

现在我的问题是 - 如何最好地安排这张表的索引?我坚持两种选择:

  1. (userId, setOn) 上的聚集索引 - 这将使我快速搜索,但我担心过度的页面拆分,因为我们将插入很多中间值(相同的用户 ID 但不同的日期)。
  2. (userId) 和 (setOn) 上的非聚集索引 - 这也会导致 (userId) 索引上的页面拆分(但它是否与第一个选项一样昂贵?)。搜索速度变慢了,因为我们使用了 NC 索引。
  3. 附加列 (id) 上的聚集索引和 (userId, setOn) 上的非聚集索引 - 这将消除数据表的页面拆分,但仍会导致 NC 索引上的一些。此选项也不是搜索的最佳选择,因为我们使用 NC 索引进行搜索。

你有什么建议?还有其他选择吗?

PS - 感谢您的时间。


经过2天的思考,我想出了一个不同的解决方案来解决这个问题。

CREATE TABLE MonthlyDiaries
(
 [userId] uniqueidentifier NOT NULL,
 [setOn] datetime NOT NULL, -- always set to GETDATE().

 [entry1_1] bigint NULL, -- FK to the 1st entry of the 1st day of the month.
 [entry1_2] bigint NULL, -- FK to the 2nd entry of the 1st day of the month.
 [entry1_3] bigint NULL,
 [entry2_1] bigint NULL,
 [entry2_2] bigint NULL,
 [entry2_3] bigint NULL,
 ...
 [entry31_1] bigint NULL,
 [entry31_2] bigint NULL,
 [entry31_3] bigint NULL,
 PRIMARY KEY (userId, setOn)
)
CREATE TABLE DiaryEntries
(
 [id] bigint IDENTITY(1,1) PRIMARY KEY CLUSTERED,
 [entry] nvarchar(255) NOT NULL
)
Run Code Online (Sandbox Code Playgroud)

基本上我将 31 天分成一行。这意味着我每个用户每月只插入一次新记录。这将页面拆分从每个用户每天 3 次减少到每个用户每月一次。显然有缺点,这里有一些

  • 行大小很大 - 但是在 99.999% 的时间里,我只从 MonthlyDiaries 中查询了一行。
  • 我可能使用了比我需要的更多的空间,因为有些日子可能没有条目。没什么大不了的。
  • 要查找特定日期的条目,需要在 DiaryEntries 上进行额外的索引查找。我相信这不会是一个很大的成本,因为我检索的行不超过 90 行,并且在 80% 的情况下我只检索了 1 行。

总的来说,我认为这是一个很好的权衡:从 3 页拆分/天/用户减少到仅 1 页拆分/月/用户,但作为回报,我的搜索速度略慢,因此付出了很小的代价。你怎么认为?

Jus*_*ing 6

首先在你的表上添加一个默认约束。其次,添加分区方案。第三次重写您最常见的查询。

聚集索引应设置为 setOn,用户 ID。这消除了索引变得碎片化的可能性。您应该使用表分区来拆分表,以便每个月都存储在一个单独的文件中。这将减少维护。可以在网上找一个分区滑动窗口脚本,每个月都可以运行,创建下个月的新表,删除最老的月份,调整分区方案。如果您不关心存储,您还可以将真正旧的月份移动到存档表中。

您的查询 where 子句应采用以下形式:

WHERE setOn > @setOn AND userId = @userId
Run Code Online (Sandbox Code Playgroud)

或者当您整月返回时:

WHERE setOn BETWEEN @setOnBegin AND @setOnEnd AND userId = @userId
Run Code Online (Sandbox Code Playgroud)

没有分区的新架构设计如下所示:

-- Stub table for foreign key
CREATE TABLE Users
(
 [userId] [uniqueidentifier] NOT NULL
  CONSTRAINT PK_Users PRIMARY KEY NONCLUSTERED
  CONSTRAINT DF_Users_userId DEFAULT NEWID(),
 [userName] VARCHAR(50) NOT NULL
)
GO

CREATE TABLE DiaryEntries
(
 [userId] [uniqueidentifier] NOT NULL
  CONSTRAINT FK_DiaryEntries_Users FOREIGN KEY REFERENCES Users,
 [setOn] [datetime] NOT NULL
  CONSTRAINT DF_DiaryEntries_setOn DEFAULT GETDATE(),
 [entry] [nvarchar](255) NULL,
 CONSTRAINT PK_DiaryEntries PRIMARY KEY CLUSTERED (setOn, userId)
)
GO
Run Code Online (Sandbox Code Playgroud)

在你开始工作后,你必须添加分区。为此,请从这篇博文开始了解一些理论。然后开始阅读这份 MSDN 白皮书。白皮书是2005年写的,2008年有分区改进我没研究过,所以2008年的解决方案可能会更简单一些。


Rem*_*anu 5

我假设您有充分的理由使用 guid 作为 id。

碎片主要是扫描的问题,而不是搜索。碎片对预读有很大的影响,并且搜索不使用也不需要预读。具有较差列选择的未碎片索引的性能总是比具有良好、可用列的 99% 碎片索引更差。如果您已经描述了扫描表的 DW 报告样式查询,那么我建议您专注于消除碎片,但是对于您描述的负载,专注于高效(覆盖)搜索和(小)范围扫描更有意义。

鉴于您的访问模式始终由@userId 驱动,这必须是聚集索引中最左侧的列。我还将 setOn 添加为聚集索引中的第二列,因为它在大多数查询中增加了一些边际值(我说边际是因为 @userId 是如此有选择性,最坏的是来自 90 百万的 90 条记录,额外的过滤由@setOn 并不重要)。我不会添加任何非聚集索引,从您描述的查询中不需要任何。

唯一的问题是删除旧记录(保留 30 天)。我建议不要使用二级 NC 索引来满足这一点。我宁愿使用滑动窗口部署每周分区方案,请参阅如何在 SQL Server 2005 上的分区表中实现自动滑动窗口。使用此解决方案,旧记录将通过分区切换删除,这可能是最有效的方式。每日分区方案将更准确地满足 30 天保留要求,也许值得尝试和测试。我犹豫直接推荐 30 个分区,因为您描述了一些查询可能会在每个分区中寻找特定的 @userId 记录,而 31 个分区可能会在重负载下产生性能问题。更好地测试和测量两者。