SQL Server 查询性能 - 聚集索引搜索

4

抱歉,本文较长,但以下是生成和填充测试工具的完整脚本。

我的测试工具包含以下表格:

|--------|  |-------------|  |-----|  |--------------|
|Column  |  |ColumnValue  |  |Row  |  |RowColumnValue|
|--------|  |-------------|  |-----|  |--------------|
|ColumnId|  |ColumnValueId|  |RowId|  |RowId         |
|Name    |  |ColumnId     |  |Name |  |ColumnValueId |
|--------|  |Value        |  |-----|  |--------------|
            |-------------|

它们代表表格中的行和列。列中单元格的可能值存储在ColumnValue中。行的选定值存储在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

7
请参阅这篇关于为什么要避免使用“元列/行”方法的优秀文章:http://www.simple-talk.com/sql/database-administration/five-simple--database-design-errors-you-should-avoid/ - 查看第三点:实体-属性-值表。 - marc_s
我理解你的观点,但我不确定我同意“那么,EAV所宣传的好处是什么呢?嗯,没有。”。对于这个例子,如果没有“元”方法,我需要创建十个表来存储ColumnValues。然后,我还需要十个多对多表来存储它们之间的关系。再加上实际上这是一个多租户数据库,Column表还包含ClientId以允许每个客户端使用不同的列,那么Create Table / Drop Table命令的数量将会非常庞大。 - Robin Day
1
如果你想从数据库中获得速度,你需要按照数据库最佳实践来处理:使用固定表格。以下是非EAV查询选择行的语句:SELECT * FROM YourTable Where xyz=... 对于你的大型PIVOT和这个查询,哪一个更快呢? - KM.
首先,我绝对不是在尝试构建一个数据库引擎来存储表格... 行/列表格名称仅仅是为了方便创建测试工具,并展示我想要的查询结果。实际的表格是一种分类数据的方式,有点像标签云,其中标签是ColumnValue,而"标签类型"则是Column表格。被标记的项目是Row表格。我将花些时间将这个实际概念放入问题中,看看是否有人能提出更好的实现方法。 - Robin Day
2
在我看来,存在一些适合使用实体-属性-值(EAV)的地方,但是用作主要特性的存储不是其中之一。EAV就像药物:在小剂量和选择的情况下,它们可以起到益处。过量会致命。在多租户应用程序中将其用作主要特性肯定是过度的。几十个表实际上很少。拥有50-100个或更多表的数据库非常常见。我认为,如果您提供有关分类系统的更多清晰度,可能会有人能够提供解决您的性能问题的备选模式。 - Thomas
虽然时间有些久远了,我有一个类似的新问题。我正在尝试摆脱EAV设计。http://stackoverflow.com/questions/3013831/db-design-pattern-many-to-many-classification-categorised-tagging - Robin Day
1个回答

2
我同意@marc_s和@KM的看法,这个大型设计从一开始就注定要失败。
微软已经投入了数百万开发人员小时来构建和调整强大的数据库引擎,但你将把所有东西塞进少量通用表中,并重新实现SQL Server已经为你设计的一切。
SQL Server已经有包含实体名称、列名称等内容的表。您通常不直接与这些系统表交互的事实是一件好事:这被称为抽象化。而且,您不太可能比SQL Server更好地实现该抽象化。
最终,采用您的方法(a)即使是最简单的查询也会变得庞大; (b) 您永远无法接近最佳性能,因为您正在放弃您本应免费获得的所有查询优化。
在不知道您的应用程序或要求的情况下,很难给出任何具体的建议。但我建议进行良好的规范化会有很大帮助。任何良好实施的非平凡数据库都有很多表;十个表加上十个xtab表不应让您望而却步。
并且不要害怕使用SQL代码生成作为在不同表之间实现公共接口的方式。即使一点点也可以帮助您很长一段时间。

首先,我绝对不是在尝试构建一个数据库引擎来存储表格... 行/列表格名称仅仅是为了方便创建测试工具,并展示我想要的查询结果。实际的表格是一种分类数据的方式,有点像标签云,其中标签是ColumnValue,而"标签类型"则是Column表格。被标记的项目是Row表格。我将花些时间将这个实际概念放入问题中,看看是否有人能提出更好的实现方法。 - Robin Day
此外,“Column”表不会是“10x”,这只是一个例子。目前“Column”表中有约3000个条目,每个条目都需要其自己的有效条目列表“ColumnValue”,从2-3个有效条目到数百个不等。每个客户端具有2-3种特定类型的标记,甚至有些将其标记分类为20种不同的方式。 - Robin Day
@Robin - 如果我过于草率,那么很抱歉。我期待着看到细节 - 请在这里添加评论,这样我就能看到修改后的帖子(或者如果您选择以这种方式进行,则是新问题)。 - Herb Caudill
虽然已经过了一段时间,我有一个类似的新问题。我正在尝试摆脱EAV设计。http://stackoverflow.com/questions/3013831/db-design-pattern-many-to-many-classification-categorised-tagging - Robin Day

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接