Die*_*cic 5 index sql-server data-pages clustered-index multi-tenant
在我的应用程序中,有几次我必须显示按某个字段分页和排序的结果。
例如,按姓氏排序的简单用户列表。正因为如此,因为我也有逻辑删除,而且它是一个多租户应用程序,我通常使用这样的集群索引:
CREATE CLUSTERED INDEX [idx] ON [Users]
(
IsDeleted ASC,
[AccountId] ASC,
[LastName] ASC
)
Run Code Online (Sandbox Code Playgroud)
这意味着像分页一样的查询SELECT TOP(20) * FROM Users WHERE IsDeleted = 0 AND AccountId = xxx按姓氏排序。我知道它不能保证被排序,但在实践中它总是如此。
然而,在这里阅读关于聚集索引的Kimberly Tripp博客文章,她说这样做是一个可怕的想法。更糟糕的是因为 IsDeleted (BIT) 字段不允许我设置
但是,如果我将 CLUSTERED INDEX 更改为唯一 ID,则需要开始使用ORDER BY LastName,这在实践中非常慢。
我的表有几百万条记录(最多几千万条),一般使用如下:
IsDeleted = 0, AccountId = xxxx(只有单个帐户的未删除数据批量更新)。题:
这类表的推荐索引(以及如何排序)是什么?
又如对于这些类型的表将是一个调查结果表包含以下几列IsDeleted (BIT), AccountId (FK GUID), UserId (FK GUID), QuestionKey (NVARCHAR), AnswerValue (TEXT),在我的聚集键是可能(IsDeleted, AccountId, UserId, QuestionKey)与99%的时间我将查询表或按3个第一场更新散装
WHERE IsDeleted = 0
AND AccountId = xxx
AND UserId = yyy
Run Code Online (Sandbox Code Playgroud)
甚至 4 个字段: ... AND QuestionKey = 'country'
编辑:
我这样做的主要原因之一是因为批量更新和查询总是限制在 1 个或少量页面。拥有一Identity列需要查询和更新才能在大多数页面中读/写。
编辑2:
按照乔·奥比什的例子:
这个查询:
SELECT TOP (20) *
FROM Users2
WHERE IsDeleted = 0
AND AccountId = '46FC5693-7446-415A-8626-8937365460D1'
ORDER BY [LastName];
Run Code Online (Sandbox Code Playgroud)
(IsDeleted, AccountId, LastName)结果中有 1 个簇索引:CPU 时间 = 3 ms,经过时间 = 3 ms。
表“用户 2”。扫描计数 1,逻辑读 5,物理读 4,预读 0,lob 逻辑读 0,lob 物理读 0,lob 预读 0。
(IsDeleted, AccountId, LastName)结果:CPU 时间 = 16 毫秒,经过时间 = 18 毫秒。
表“用户 2”。扫描计数 1,逻辑读取 533,物理读取 5,预读读取 1240,lob 逻辑读取 0,lob 物理读取 0,lob 预读读取 0。
注意 IO 和时间。如果数据没有存储在一起,它会更慢并且需要更多的IO。它可能需要更多空间,但速度差异是显着的。我这样做有错吗?
首先,我需要重申RDFozz在他关于包含显式ORDER BY. 如果 SQL Server 执行分配顺序扫描,您可能会得到错误的结果。包含ORDER BY在查询中不会导致性能下降。为什么不这样做?
从查询性能的角度来看,您想要的索引取决于您一次返回的行数以及表中实际需要的列数。
首先,我将把大约 650 万行数据放入一个有 6 个租户的表中:
CREATE TABLE dbo.Users2 (
IsDeleted Bit NOT NULL,
[AccountId] UNIQUEIDENTIFIER NOT NULL,
[LastName] NVARCHAR(50) NOT NULL,
[UsefulColumn] NVARCHAR(20) NOT NULL,
[OtherColumns] NVARCHAR(100) NOT NULL
);
CREATE CLUSTERED INDEX [idx] ON [Users2]
(
IsDeleted ASC,
[AccountId] ASC,
[LastName] ASC
);
CREATE TABLE #ids (id INT NOT NULL IDENTITY (0, 1), [AccountId] UNIQUEIDENTIFIER NOT NULL);
INSERT INTO #ids
SELECT TOP 6 NEWID()
FROM master..spt_values;
INSERT INTO [Users2] WITH (TABLOCK)
SELECT
CASE WHEN t1.number % 10 = 1 THEN 1 ELSE 0 END
, #ids.[AccountId]
, LEFT(REPLACE(CONVERT(NVARCHAR(50), NEWID()), '-', ''), 12)
, REPLICATE(N'Z', 20)
, REPLICATE(N'Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
LEFT OUTER JOIN #ids ON ABS(t1.number % 6) = #ids.id;
DROP TABLE #ids;
-- get an ID: FFA7D6D8-63E8-422B-B5E7-F7020871CDB4
SELECT TOP 1 [AccountId] FROM Users2
WHERE IsDeleted = 0
ORDER BY [AccountId] DESC;
Run Code Online (Sandbox Code Playgroud)
运行类似于您的查询时:
SELECT TOP (20) *
FROM Users2
WHERE IsDeleted = 0
AND AccountId = 'FFA7D6D8-63E8-422B-B5E7-F7020871CDB4'
ORDER BY [LastName];
Run Code Online (Sandbox Code Playgroud)
我按预期得到聚集索引:
使用聚集索引是最好的选择吗?这取决于。如果您不需要从表中选择每一列,那么您可以定义一个较小的覆盖索引,该索引无需显式排序即可返回您需要的数据。当您进一步分页到数据中时,具有较小的覆盖索引有利于性能。假设您只需要UsefulColumn而不是OtherColumns列。您可以定义以下索引:
CREATE NONCLUSTERED INDEX [idx_1] ON [Users2]
(
[AccountId] ASC,
[LastName] ASC
)
INCLUDE ([UsefulColumn])
WHERE IsDeleted = 0;
Run Code Online (Sandbox Code Playgroud)
它相当大。对于我的测试用例,它大约是数据大小的 28%。对于这个索引,需要注意的是,改变表的聚集键不会对其大小产生很大的影响。SQL Server 将聚集键列存储在索引的叶节点中,除非它们已包含在索引中。这可以通过一个简单的测试来证明:
CREATE TABLE dbo.IX_TEST (
COL1 BIGINT NOT NULL,
COL2 BIGINT NOT NULL,
FILLER VARCHAR(6) NOT NULL,
PRIMARY KEY (COL1)
);
INSERT INTO dbo.IX_TEST WITH (TABLOCK)
SELECT TOP (1000000)
ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
, ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
, REPLICATE('Z', 6)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;
EXEC sp_spaceused 'IX_TEST'; -- 96 KB
CREATE INDEX COL1 ON dbo.IX_TEST (COL1)
EXEC sp_spaceused 'IX_TEST'; -- 14032 KB
CREATE INDEX COL2 ON dbo.IX_TEST (COL2)
EXEC sp_spaceused 'IX_TEST'; --- 35920 KB
Run Code Online (Sandbox Code Playgroud)
回到我们的表,如果我只在UsefulColumn列上创建索引,那么列的开销(没有压缩)大约为 1 个字节IsDeleted,16 个字节用于AccountId2 *LastName列的平均长度,0 或内部 uniquifier 的 4 个字节(仅适用于重复的姓氏)。对于我的测试数据,这是相当多的开销:
1 + 16 + 2 * 12 + 0 = 41 个字节
然而,对于idx_1我在上面定义的索引,它大约只有 1 个字节(1 代表IsDeleted0 代表 uniquifier,假设没有很多重复的姓氏)。索引很大,因为我使用宽列作为键列。请记住,索引的大小将与您当前的集群键相同,但是将表的集群键更改为一组更细的列将大大减少仅在 上定义的索引的大小UsefulColumn。
对于任何TOP值,我都应该在覆盖索引上获得一个很好的索引查找。这个查询:
SELECT TOP (200000) [LastName], [UsefulColumn]
FROM Users2
WHERE IsDeleted = 0 AND AccountId = 'FFA7D6D8-63E8-422B-B5E7-F7020871CDB4'
ORDER BY [LastName];
Run Code Online (Sandbox Code Playgroud)
有以下计划:
即使您需要表中的每一列,上面定义的索引仍然可以提供足够好的性能。您将获得对返回的每一行的键查找。对于单个最终用户,在 20 行上进行键查找应该不会引起注意。在您的测试中,您看到执行时间分别为 3 毫秒和 18 毫秒。但是,如果您有一个高度并发的工作负载,它可能会有所作为。只有你才能正确评估。
即使没有上面的警告,在选择许多行时,您可能会注意到大的性能差异:
第一个查询有一个索引提示来强制使用索引。查询优化器认为该计划将比使用聚集索引的计划昂贵得多。
如果我不得不猜测,我会说您可能不需要使用聚集索引来执行分页。如果表中有很多索引,您可能会受益于为分页查询创建新的覆盖索引以及在较小的唯一列集上定义聚集索引。
| 归档时间: |
|
| 查看次数: |
3762 次 |
| 最近记录: |