SQL Server 查询性能 - 聚集索引查找
Posted
技术标签:
【中文标题】SQL Server 查询性能 - 聚集索引查找【英文标题】:SQL Server Query Performance - Clustered Index Seek 【发布时间】:2010-03-05 12:18:31 【问题描述】:请原谅这篇长文,但下面我包含了一个完整的脚本来生成和填充我的测试工具。
我的测试工具有以下表格
|--------| |-------------| |-----| |--------------|
|Column | |ColumnValue | |Row | |RowColumnValue|
|--------| |-------------| |-----| |--------------|
|ColumnId| |ColumnValueId| |RowId| |RowId |
|Name | |ColumnId | |Name | |ColumnValueId |
|--------| |Value | |-----| |--------------|
|-------------|
它们代表表格中的行和列。列中单元格的可能值存储在 ColumnValue 中。为 Row 选择的值存储在 RowColumnValue 中。 (我希望这很清楚)
我已使用 10 列、10,000 行、每列 50 个列值 (500) 和每行 25 个选定列值 (250,000) 填充数据。
我有一些动态 sql,它返回所有行,以列为中心,并包含每列的选定列值的 XML 列表。
注意:出于性能测试目的,我将查询包装在 SELECT COUNT(*)
中,这样查询就不会通过网络返回大量数据。
我的测试工具在大约 5-6 秒内运行此查询(使用计数)。执行计划显示 92% 的查询花费在 [ColumnValue].[PK_ColumnValue]
上的聚集索引搜索上。客户端统计显示客户端处理时间、总执行时间和服务器回复的等待时间都为 0。
我意识到 RowColumnValue 表中的 250k 行非常多,我可能对 SQL Server 期望过高。但是,我的期望是查询应该能够运行得比这快得多。或者至少执行计划应该呈现不同的瓶颈,而不是聚集索引搜索。
谁能阐明这个问题或给我一些关于如何提高效率的建议?
运行数据透视表以显示表格的动态 SQL:
DECLARE @columnDataList NVARCHAR(MAX)
SELECT
@columnDataList =
CAST
(
(
SELECT
', CONVERT(xml, [PVT].[' + [Column].[Name] + ']) [Column.' + [Column].[Name] + ']'
FROM
[Column]
ORDER BY
[Column].[Name]
FOR XML PATH('')
) AS XML
).value('.', 'NVARCHAR(MAX)')
DECLARE @columnPivotList NVARCHAR(MAX)
SELECT
@columnPivotList =
CAST
(
(
SELECT
', [' + [Column].[Name] + ']'
FROM
[Column]
ORDER BY
[Column].[Name]
FOR XML PATH('')
) AS XML
).value('.', 'NVARCHAR(MAX)')
EXEC('
SELECT
COUNT(*)
FROM
(
SELECT
[PVT].[RowId]
' + @columnDataList + '
FROM
(
SELECT
[Row].[RowId],
[Column].[Name] [ColumnName],
[XmlRowColumnValues].[XmlRowColumnValues] [XmlRowColumnValues]
FROM
[Row]
CROSS JOIN
[Column]
CROSS APPLY
(
SELECT
[ColumnValue].[Value] [Value]
FROM
[RowColumnValue]
INNER JOIN
[ColumnValue]
ON
[ColumnValue].[ColumnValueId] = [RowColumnValue].[ColumnValueId]
WHERE
[RowColumnValue].[RowId] = [Row].[RowId]
AND
[ColumnValue].[ColumnId] = [Column].[ColumnId]
FOR XML PATH (''''), ROOT(''Values'')
) [XmlRowColumnValues] ([XmlRowColumnValues])
) [PivotData]
PIVOT
(
MAX([PivotData].[XmlRowColumnValues])
FOR
[ColumnName]
IN
([0]' + @columnPivotList + ')
) PVT
) RowColumnData
')
生成和填充数据库的脚本:
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Row](
[RowId] [int] IDENTITY(1,1) NOT NULL,
[Name] [nvarchar](50) NOT NULL,
CONSTRAINT [PK_Row] PRIMARY KEY CLUSTERED
(
[RowId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Column](
[ColumnId] [int] IDENTITY(1,1) NOT NULL,
[Name] [nvarchar](50) NOT NULL,
CONSTRAINT [PK_Column] PRIMARY KEY CLUSTERED
(
[ColumnId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[RowColumnValue](
[RowId] [int] NOT NULL,
[ColumnValueId] [int] NOT NULL,
CONSTRAINT [PK_RowColumnValue] PRIMARY KEY CLUSTERED
(
[RowId] ASC,
[ColumnValueId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[ColumnValue](
[ColumnValueId] [int] IDENTITY(1,1) NOT NULL,
[ColumnId] [int] NOT NULL,
[Value] [nvarchar](50) NOT NULL,
CONSTRAINT [PK_ColumnValue] PRIMARY KEY CLUSTERED
(
[ColumnValueId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
CREATE NONCLUSTERED INDEX [FKIX_ColumnValue_ColumnId] ON [dbo].[ColumnValue]
(
[ColumnId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO
ALTER TABLE [dbo].[ColumnValue] WITH CHECK ADD CONSTRAINT [FK_ColumnValue_Column] FOREIGN KEY([ColumnId])
REFERENCES [dbo].[Column] ([ColumnId])
GO
ALTER TABLE [dbo].[ColumnValue] CHECK CONSTRAINT [FK_ColumnValue_Column]
GO
ALTER TABLE [dbo].[RowColumnValue] WITH CHECK ADD CONSTRAINT [FK_RowColumnValue_ColumnValue] FOREIGN KEY([ColumnValueId])
REFERENCES [dbo].[ColumnValue] ([ColumnValueId])
GO
ALTER TABLE [dbo].[RowColumnValue] CHECK CONSTRAINT [FK_RowColumnValue_ColumnValue]
GO
ALTER TABLE [dbo].[RowColumnValue] WITH CHECK ADD CONSTRAINT [FK_RowColumnValue_Row] FOREIGN KEY([RowId])
REFERENCES [dbo].[Row] ([RowId])
GO
ALTER TABLE [dbo].[RowColumnValue] CHECK CONSTRAINT [FK_RowColumnValue_Row]
GO
DECLARE @columnLoop INT
DECLARE @columnValueLoop INT
DECLARE @rowLoop INT
DECLARE @columnId INT
DECLARE @columnValueId INT
DECLARE @rowId INT
SET @columnLoop = 0
WHILE @columnLoop < 10
BEGIN
INSERT INTO [Column] ([Name]) VALUES(NEWID())
SET @columnId = @@IDENTITY
SET @columnValueLoop = 0
WHILE @columnValueLoop < 50
BEGIN
INSERT INTO [ColumnValue] ([ColumnId], [Value]) VALUES(@columnId, NEWID())
SET @columnValueLoop = @columnValueLoop + 1
END
SET @columnLoop = @columnLoop + 1
END
SET @rowLoop = 0
WHILE @rowLoop < 10000
BEGIN
INSERT INTO [Row] ([Name]) VALUES(NEWID())
SET @rowId = @@IDENTITY
INSERT INTO [RowColumnValue] ([RowId], [ColumnValueId]) SELECT TOP 25 @rowId, [ColumnValueId] FROM [ColumnValue] ORDER BY NEWID()
SET @rowLoop = @rowLoop + 1
END
【问题讨论】:
查看这篇优秀的帖子,了解为什么要避免这种“元列/行”方法:simple-talk.com/sql/database-administration/… - 查看#3:实体-属性-值表 我明白了要点,但我不确定我是否同意“那么吹捧 EAV 的好处是什么?嗯,没有。”尽管。如果没有“元”方法,对于这个例子,我需要创建十个表来存储 ColumnValues。然后我需要十个多到多表来存储它们之间的关系。除此之外,实际上这是一个多租户数据库,并且 Column 表还包含一个 ClientId 以允许每个客户端使用不同的列,那么 Create Table / Drop Table 命令的数量将是巨大的。 如果您想从数据库中获得速度,您需要了解数据库的最佳工作方式:固定表。这是用于选择行的非 EAV 查询:SELECT * FROM YourTable Where xyz=...
您的巨大 PIVOT 或这个会更快?
对于初学者,我绝不会尝试构建数据库引擎来存储表...行/列表名称纯粹是为了方便创建测试工具并展示我如何希望它为有问题的查询返回。实际的表是一种对数据进行分类的方式,有点像标签云,标签是 ColumnValue 的,另外还有一个“标签类型”,即 Column 表。被标记的项目是 Row 表。我将花一些时间将这个实际概念放入问题中,看看是否有人可以提出更好的实现。
国际海事组织,世界上有一些选择 EAV 的地方,但主要功能的存储不是其中之一。 EAV 就像药物:在少量和特定情况下,它们可能是有益的。太多会杀了你。在多租户应用程序中用作主要功能肯定太多了。坦率地说,几十张桌子很小。拥有 50-100 个或更多表的数据库是很常见的。我认为,如果您对分类系统提供更清晰的说明,那么有人可能会提供一种替代模式,从而解决您的性能问题。
【参考方案1】:
我同意@marc_s 和@KM 的观点,即这个宏伟的设计从一开始就注定要失败。
Microsoft 的数百万开发人员花费在构建和微调强大而强大的数据库引擎上,但您将通过将所有内容塞进少量通用表并重新实现 SQL 的所有内容来彻底改造它Server 已经为您设计好了。
SQL Server 已经有包含实体名称、列名称等的表。您通常不直接与这些系统表交互这一事实是一件好事:它被称为抽象。在实现该抽象方面,您不太可能比 SQL Server 做得更好。
归根结底,使用您的方法 (a) 即使是最简单的查询也将是可怕的;并且 (b) 您永远不会接近最佳性能,因为您放弃了所有可以免费获得的查询优化。
如果不了解您的应用程序或您的要求,很难给出任何具体的建议。但我建议一些好的旧规范化会有很长的路要走。任何实现良好的、重要的数据库都有很多表;十张桌子加十张 xtab 桌子不应该吓跑你。
不要害怕将 SQL 代码生成作为跨不同表实现通用接口的一种方式。一点点可以走很长的路。
【讨论】:
对于初学者,我绝不会尝试构建数据库引擎来存储表...行/列表名称纯粹是为了方便创建测试工具并展示我如何希望它为有问题的查询返回。实际的表是一种对数据进行分类的方式,有点像标签云,标签是 ColumnValue 的,另外还有一个“标签类型”,即 Column 表。被标记的项目是 Row 表。我将花一些时间将这个实际概念放入问题中,看看是否有人可以提出更好的实现。 补充一点,“列”表不会是“10x”,这只是在这个例子中。 “列”表中目前有大约 3000 个条目,每个条目都需要它自己的有效条目列表“ColumnValue”,从 2-3 个有效条目到数百个不等。每个客户都有从 2-3 种特定类型的标记到以 20 种不同方式对其标记进行分类的标记。 @Robin - 抱歉,如果我半途而废。我期待看到详细信息 - 请在此处添加评论,以便我看到修改后的帖子(或新问题,如果您选择这样做)。 虽然过了一段时间,但我有一个类似性质的新问题。我正在尝试摆脱 EAV 设计。 ***.com/questions/3013831/…以上是关于SQL Server 查询性能 - 聚集索引查找的主要内容,如果未能解决你的问题,请参考以下文章