4

我们有一个将数据存储在 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 函数生成(比如我们的例子)是一个非常糟糕的选择作为聚集索引两个原因:

  1. 碎片化
  2. 尺寸

我也知道一个好的集群关键是它是:

  1. 独特的,
  2. 狭窄,
  3. 静止的,
  4. 不断增加,
  5. 不可为空,
  6. 和固定宽度

有关这背后原因的更多详细信息,请查看以下精彩视频: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;

执行计划

4

1 回答 1

2

我个人使用我INT IDENTITY的大部分主键和集群键。

您需要将主键分开,这是一个逻辑结构 - 它唯一标识您的行,它必须是唯一且稳定的,并且NOT NULL. AGUID也适用于主键 - 因为它保证是唯一的。GUID如果您使用 SQL Server 复制,作为主键的 A 是一个不错的选择,因为在这种情况下,无论如何您都需要一个唯一标识列GUID

SQL Server 中的集群键是一种物理构造,用于对数据进行物理排序,要正确执行要困难得多。通常, SQL Server 上的索引女王Kimberly Tripp 也需要一个好的集群键是唯一的、稳定的、尽可能窄的,并且理想情况下是不断增长的(这INT IDENTITY是)。

在此处查看她关于索引的文章:

并参见 Jimmy Nilsson 的The Cost of GUIDs as Primary Key

AGUID对于集群键来说是一个非常糟糕的选择,因为它很宽,完全随机,因此会导致索引碎片和性能不佳。此外,集群键行也存储在每个非集群(附加)索引的每个条目中,所以你真的想保持小 -GUID是 16 字节与INT4 字节,并且有几个非聚集索引和几百万行,这有很大的不同。

在 SQL Server 中,默认情况下您的主键是您的集群键 - 但它不是必须的。您可以轻松地使用 aGUID作为您的非集群主键,并将 aINT IDENTITY作为集群键 - 只需稍加注意即可。

于 2013-05-18T17:16:53.823 回答