我们有一个将数据存储在 SQL Server 数据库中的应用程序。(目前我们支持 SQL Server 2005 及更高版本)。我们的数据库有 400 多张表。数据库的结构并不理想。最大的问题是我们有很多以 GUID (NEWID()) 作为主集群键的表。当我问我们的主要数据库架构师“为什么?”时,他说:“这是因为复制”。我们的数据库应该支持事务复制。最初,所有主键都是 INT IDENTITY(1,1) CLUSTERED。但后来当涉及到复制支持时,这个字段被 UNIQUEIDENTIFIER DEFAULT NEWID() 取代。他说“否则处理复制将是一场噩梦”。当时 SQL 7/2000 不支持 NEWSEQUENTIALID()。所以现在我们有具有以下结构的表:
CREATE TABLE Table1(
Table1_PID uniqueidentifier DEFAULT NEWID() NOT NULL,
Field1 varchar(50) NULL,
FieldN varchar(50) NULL,
CONSTRAINT PK_Table1 PRIMARY KEY CLUSTERED (Table1_PID)
)
GO
CREATE TABLE Table2(
Table2_PID uniqueidentifier DEFAULT NEWID() NOT NULL,
Table1_PID uniqueidentifier NULL,
Field1 varchar(50) NULL,
FieldN varchar(50) NULL,
CONSTRAINT PK_Table2 PRIMARY KEY CLUSTERED (Table2_PID),
CONSTRAINT FK_Table2_Table1 FOREIGN KEY (Table1_PID) REFERENCES Table1 (Table1_PID)
)
GO
所有的表实际上都有很多字段(最多 35 个)和最多 15 个非聚集索引。
我知道一个非连续的 GUID——比如它的值在客户端生成(使用 .NET)或由 NEWID() SQL 函数生成(比如我们的例子)是一个非常糟糕的选择作为聚集索引两个原因:
- 碎片化
- 尺寸
我也知道一个好的集群关键是它是:
- 独特的,
- 狭窄,
- 静止的,
- 不断增加,
- 不可为空,
- 和固定宽度
有关这背后原因的更多详细信息,请查看以下精彩视频:http ://technet.microsoft.com/en-us/sqlserver/gg508879.aspx 。
所以,INT IDENTITY 确实是最好的选择。BIGINT IDENTITY 也很好,但通常具有 2+ 十亿行的 INT 对于绝大多数表来说应该足够了。
当我们的客户开始遭受碎片化的困扰时,我们决定将主键设为非集群。结果,这些表仍然没有聚集索引。换句话说,这些表变成了 HEAPS。我个人不喜欢这个解决方案,因为我确信堆表不是一个好的数据库设计的一部分。请查看此 SQL Server 最佳实践文章:http ://technet.microsoft.com/en-us/library/cc917672.aspx 。
目前我们考虑两种方案来改进数据库结构:
第一个选项是将 DEFAULT NEWID() 替换为 DEFAULT NEWSEQUENTIALID() 用于主集群键:
CREATE TABLE Table1_GUID ( Table1_PID uniqueidentifier DEFAULT NEWSEQUENTIALID() NOT NULL, Field1 varchar(50) NULL, FieldN varchar(50) NULL, CONSTRAINT PK_Table1 PRIMARY KEY CLUSTERED (Table1_PID) ) GO
第二个选项是将 INT IDENTITY 列添加到每个表并使其成为 CLUSTERED UNIQUE 索引,使主键不聚集。所以 Table1 看起来像:
CREATE TABLE Table1_INT ( Table1_ID int IDENTITY(1,1) NOT NULL, Table1_PID uniqueidentifier DEFAULT NEWSEQUENTIALID() NOT NULL, Field1 varchar(50) NULL, FieldN varchar(50) NULL, CONSTRAINT PK_Table1 PRIMARY KEY NONCLUSTERED (Table1_PID), CONSTRAINT UK_Table1 UNIQUE CLUSTERED (Table1_ID) ) GO
Table1_PID 将用于复制,(这就是我们将其保留为 PK 的原因),而 Table1_ID 根本不会被复制。
长话短说,在我们运行基准测试以查看哪种方法更好之后,我们发现两种解决方案都不好:
第一种方法(Table1_GUID)揭示了以下缺点:虽然顺序 GUID 肯定比常规随机 GUID 好很多,但它们仍然比 INT 大四倍(16 对 4 字节),这是我们案例中的一个因素,因为我们有我们的表中有很多行(最多 6000 万行),以及这些表上的很多非聚集索引(最多 15 个)。集群键被添加到每个非聚集索引中,因此显着增加了 16 字节与 4 字节大小的负面影响。更多字节意味着磁盘和 SQL Server RAM 中的页面更多,因此更多的磁盘 I/O 和更多的 SQL Server 工作。
更准确地说,在我向每个表插入 2500 万行真实数据,然后在每个表上创建 15 个非聚集索引后,我看到表使用的空间存在很大差异:
EXEC sp_spaceused 'Table1_GUID' -- 14.85 GB
EXEC sp_spaceused 'Table1_INT' -- 11.68 GB
此外,测试表明插入 Table1_GUID 比插入 Table1_INT 慢一点。
第二种方法(Table1_INT)显示,在大多数查询(SELECT)中,在 Table1_INT.Table1_PID = Table2_INT.Table1_PID 执行计划上连接两个表变得更糟,因为出现了额外的 Key Lookup 运算符。
现在的问题是:我相信我们的问题应该有更好的解决方案。如果您能向我推荐一些东西或向我指出一个好的资源,我将不胜感激。先感谢您。
更新:
让我举一个 SELECT 语句的示例,其中出现了附加的 Key Lookup 运算符:
--Create 2 tables with int IDENTITY(1,1) as CLUSTERED KEY.
--These tables have one-to-many relationship.
CREATE TABLE Table1_INT (
Table1_ID int IDENTITY(1,1) NOT NULL,
Table1_PID uniqueidentifier DEFAULT NEWSEQUENTIALID() NOT NULL,
Field1 varchar(50) NULL,
FieldN varchar(50) NULL,
CONSTRAINT PK_Table1_INT PRIMARY KEY NONCLUSTERED (Table1_PID),
CONSTRAINT UK_Table1_INT UNIQUE CLUSTERED (Table1_ID)
)
GO
CREATE TABLE Table2_INT(
Table2_ID int IDENTITY(1,1) NOT NULL,
Table2_PID uniqueidentifier DEFAULT NEWSEQUENTIALID() NOT NULL,
Table1_PID uniqueidentifier NULL,
Field1 varchar(50) NULL,
FieldN varchar(50) NULL,
CONSTRAINT PK_Table2_INT PRIMARY KEY NONCLUSTERED (Table2_PID),
CONSTRAINT UK_Table2_INT UNIQUE CLUSTERED (Table2_ID),
CONSTRAINT FK_Table2_Table1_INT FOREIGN KEY (Table1_PID) REFERENCES Table1_INT (Table1_PID)
)
GO
并创建其他两个表进行比较:
--Create the same 2 tables, BUT with uniqueidentifier NEWSEQUENTIALID() as CLUSTERED KEY.
CREATE TABLE Table1_GUID (
Table1_PID uniqueidentifier DEFAULT NEWSEQUENTIALID() NOT NULL,
Field1 varchar(50) NULL,
FieldN varchar(50) NULL,
CONSTRAINT PK_Table1_GUID PRIMARY KEY CLUSTERED (Table1_PID),
)
GO
CREATE TABLE Table2_GUID(
Table2_PID uniqueidentifier DEFAULT NEWSEQUENTIALID() NOT NULL,
Table1_PID uniqueidentifier NULL,
Field1 varchar(50) NULL,
FieldN varchar(50) NULL,
CONSTRAINT PK_Table2_GUID PRIMARY KEY CLUSTERED (Table2_PID),
CONSTRAINT FK_Table2_Table1_GUID FOREIGN KEY (Table1_PID) REFERENCES Table1_GUID (Table1_PID)
)
GO
现在运行以下选择语句并查看执行计划以进行比较:
SELECT T1.Field1, T2.FieldN
FROM Table1_INT T1
INNER JOIN Table2_INT T2
ON T1.Table1_PID = T2.Table1_PID;
SELECT T1.Field1, T2.FieldN
FROM Table1_GUID T1
INNER JOIN Table2_GUID T2
ON T1.Table1_PID = T2.Table1_PID;