对于使用事务复制发布的表的聚簇索引,哪种数据类型最佳?

时间:2013-05-18 17:10:52

标签: sql sql-server transactional-replication

我们有一个将数据存储在SQL Server数据库中的应用程序。 (目前我们支持SQL Server 2005及更高版本)。我们的DB有超过400个表。数据库的结构并不理想。最大的问题是我们有很多带有GUID(NEWID())的表作为主CLUSTERED键。当我问我们的主数据库架构师“为什么?”时,他说:“这是因为复制”。我们的数据库应该支持事务复制。最初,所有主键都是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函数生成的GUID(就像我们的情况一样)是一个非常糟糕的选择聚集索引有两个原因:

  1. 碎片
  2. 尺寸
  3. 我也知道好的聚类关键是:

    1. unique,
    2. 窄,
    3. 静态的,
    4. 不断增加,
    5. 不可为空的,
    6. 和固定宽度
    7. 有关此背后原因的详细信息,请查看以下精彩视频:http://technet.microsoft.com/en-us/sqlserver/gg508879.aspx

      所以,INT IDENTITY确实是最好的选择。 BIGINT IDENTITY也很好,但是对于绝大多数表来说,通常一个行数超过20亿的INT就足够了。

      当我们的客户开始遭受碎片化时,决定将主键设置为非群集。结果,这些表保持没有聚簇索引。换句话说,这些表格变成了HEAPS。我个人不喜欢这个解决方案,因为我确信堆表不是一个好的数据库设计的一部分。请检查此SQL Server最佳实践文章:http://technet.microsoft.com/en-us/library/cc917672.aspx

      目前我们考虑两种改进数据库结构的方法:

      第一个选项是用主要群集密钥的DEFAULT NEWSEQUENTIALID()替换DEFAULT NEWID():

      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工作。

      更准确地说,在我向每个表插入25mln行真实数据然后在每个表上创建了15个非聚集索引之后,我看到表中使用的空间有很大差异:

      EXEC sp_spaceused 'Table1_GUID' -- 14.85 GB
      EXEC sp_spaceused 'Table1_INT' -- 11.68 GB
      

      此外,测试显示INSERT到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
      

      为comperison创建其他两个表:

      --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语句并查看要比较的执行计划:

      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;
      

      Execution plan

1 个答案:

答案 0 :(得分:1)

我个人使用INT IDENTITY来处理我的主要和群集密钥。

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

SQL Server中的集群密钥是一个物理结构,用于数据的物理排序,并且更难以正确使用。通常,SQL Server上的索引女王,Kimberly Tripp,也需要一个好的聚类密钥,以便尽可能地独特,稳定,并且理想地不断增加(INT IDENTITY GUID是)。

请参阅她关于索引的文章:

还可以看到Jimmy Nilsson的The Cost of GUIDs as Primary Key

对于群集密钥来说,GUID是一个非常糟糕的选择,因为它很宽,完全随机,因此会导致错误的索引碎片和糟糕的性能。此外,群集密钥行也存储在每个非群集(附加)索引的每个条目中,因此您确实希望保持较小 - INT是16字节而不是{{ 1}}是4字节,并且有几个非聚集索引和几百万行,这会产生巨大差异。

在SQL Server中,您的主键默认情况下是您的群集密钥 - 但它不一定是。您可以轻松地使用GUID作为非群集主键,使用INT IDENTITY作为群集密钥 - 只需要了解一点。