与代理整数键相比,自然键在 SQL Server 中提供的性能更高还是更低?

Han*_*non 28 performance sql-server sql-server-2012 surrogate-key natural-key performance-testing

我是代理键的粉丝。我的发现存在确认偏倚的风险。

我在这里和http://stackoverflow.com 上看到的许多问题都使用自然键而不是基于IDENTITY()值的代理键。

我的计算机系统背景告诉我,对整数执行任何比较运算都比比较字符串快。

这个评论让我怀疑我的信念,所以我想我会创建一个系统来研究我的论点,即整数比字符串更快,用作 SQL Server 中的键。

由于小数据集可能几乎没有可辨别的差异,我立即想到了一个两表设置,其中主表有 1,000,000 行,而辅助表在主表中的每一行有 10 行,总共有 10,000,000 行。次要表。我的测试的前提是创建两组这样的表,一组使用自然键,一组使用整数键,并在简单的查询上运行计时测试,例如:

SELECT *
FROM Table1
    INNER JOIN Table2 ON Table1.Key = Table2.Key;
Run Code Online (Sandbox Code Playgroud)

以下是我作为测试台创建的代码:

USE Master;
IF (SELECT COUNT(database_id) FROM sys.databases d WHERE d.name = 'NaturalKeyTest') = 1
BEGIN
    ALTER DATABASE NaturalKeyTest SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
    DROP DATABASE NaturalKeyTest;
END
GO
CREATE DATABASE NaturalKeyTest 
    ON (NAME = 'NaturalKeyTest', FILENAME = 
        'C:\SQLServer\Data\NaturalKeyTest.mdf', SIZE=8GB, FILEGROWTH=1GB) 
    LOG ON (NAME='NaturalKeyTestLog', FILENAME = 
        'C:\SQLServer\Logs\NaturalKeyTest.mdf', SIZE=256MB, FILEGROWTH=128MB);
GO
ALTER DATABASE NaturalKeyTest SET RECOVERY SIMPLE;
GO
USE NaturalKeyTest;
GO
CREATE VIEW GetRand
AS 
    SELECT RAND() AS RandomNumber;
GO
CREATE FUNCTION RandomString
(
    @StringLength INT
)
RETURNS NVARCHAR(max)
AS
BEGIN
    DECLARE @cnt INT = 0
    DECLARE @str NVARCHAR(MAX) = '';
    DECLARE @RandomNum FLOAT = 0;
    WHILE @cnt < @StringLength
    BEGIN
        SELECT @RandomNum = RandomNumber
        FROM GetRand;
        SET @str = @str + CAST(CHAR((@RandomNum * 64.) + 32) AS NVARCHAR(MAX)); 
        SET @cnt = @cnt + 1;
    END
    RETURN @str;
END;
GO
CREATE TABLE NaturalTable1
(
    NaturalTable1Key NVARCHAR(255) NOT NULL 
        CONSTRAINT PK_NaturalTable1 PRIMARY KEY CLUSTERED 
    , Table1TestData NVARCHAR(255) NOT NULL 
);
CREATE TABLE NaturalTable2
(
    NaturalTable2Key NVARCHAR(255) NOT NULL 
        CONSTRAINT PK_NaturalTable2 PRIMARY KEY CLUSTERED 
    , NaturalTable1Key NVARCHAR(255) NOT NULL 
        CONSTRAINT FK_NaturalTable2_NaturalTable1Key 
        FOREIGN KEY REFERENCES dbo.NaturalTable1 (NaturalTable1Key) 
        ON DELETE CASCADE ON UPDATE CASCADE
    , Table2TestData NVARCHAR(255) NOT NULL  
);
GO

/* insert 1,000,000 rows into NaturalTable1 */
INSERT INTO NaturalTable1 (NaturalTable1Key, Table1TestData) 
    VALUES (dbo.RandomString(25), dbo.RandomString(100));
GO 1000000 

/* insert 10,000,000 rows into NaturalTable2 */
INSERT INTO NaturalTable2 (NaturalTable2Key, NaturalTable1Key, Table2TestData)
SELECT dbo.RandomString(25), T1.NaturalTable1Key, dbo.RandomString(100)
FROM NaturalTable1 T1
GO 10 

CREATE TABLE IDTable1
(
    IDTable1Key INT NOT NULL CONSTRAINT PK_IDTable1 
    PRIMARY KEY CLUSTERED IDENTITY(1,1)
    , Table1TestData NVARCHAR(255) NOT NULL 
    CONSTRAINT DF_IDTable1_TestData DEFAULT dbo.RandomString(100)
);
CREATE TABLE IDTable2
(
    IDTable2Key INT NOT NULL CONSTRAINT PK_IDTable2 
        PRIMARY KEY CLUSTERED IDENTITY(1,1)
    , IDTable1Key INT NOT NULL 
        CONSTRAINT FK_IDTable2_IDTable1Key FOREIGN KEY 
        REFERENCES dbo.IDTable1 (IDTable1Key) 
        ON DELETE CASCADE ON UPDATE CASCADE
    , Table2TestData NVARCHAR(255) NOT NULL 
        CONSTRAINT DF_IDTable2_TestData DEFAULT dbo.RandomString(100)
);
GO
INSERT INTO IDTable1 DEFAULT VALUES;
GO 1000000
INSERT INTO IDTable2 (IDTable1Key)
SELECT T1.IDTable1Key
FROM IDTable1 T1
GO 10
Run Code Online (Sandbox Code Playgroud)

上面的代码创建了一个数据库和 4 个表,并用数据填充这些表,准备测试。我运行的测试代码是:

USE NaturalKeyTest;
GO
DECLARE @loops INT = 0;
DECLARE @MaxLoops INT = 10;
DECLARE @Results TABLE (
    FinishedAt DATETIME DEFAULT (GETDATE())
    , KeyType NVARCHAR(255)
    , ElapsedTime FLOAT
);
WHILE @loops < @MaxLoops
BEGIN
    DBCC FREEPROCCACHE;
    DBCC FREESESSIONCACHE;
    DBCC FREESYSTEMCACHE ('ALL');
    DBCC DROPCLEANBUFFERS;
    WAITFOR DELAY '00:00:05';
    DECLARE @start DATETIME = GETDATE();
    DECLARE @end DATETIME;
    DECLARE @count INT;
    SELECT @count = COUNT(*) 
    FROM dbo.NaturalTable1 T1
        INNER JOIN dbo.NaturalTable2 T2 ON T1.NaturalTable1Key = T2.NaturalTable1Key;
    SET @end = GETDATE();
    INSERT INTO @Results (KeyType, ElapsedTime)
    SELECT 'Natural PK' AS KeyType, CAST((@end - @start) AS FLOAT) AS ElapsedTime;

    DBCC FREEPROCCACHE;
    DBCC FREESESSIONCACHE;
    DBCC FREESYSTEMCACHE ('ALL');
    DBCC DROPCLEANBUFFERS;
    WAITFOR DELAY '00:00:05';
    SET @start = GETDATE();
    SELECT @count = COUNT(*) 
    FROM dbo.IDTable1 T1
        INNER JOIN dbo.IDTable2 T2 ON T1.IDTable1Key = T2.IDTable1Key;
    SET @end = GETDATE();
    INSERT INTO @Results (KeyType, ElapsedTime)
    SELECT 'IDENTITY() PK' AS KeyType, CAST((@end - @start) AS FLOAT) AS ElapsedTime;

    SET @loops = @loops + 1;
END
SELECT KeyType, FORMAT(CAST(AVG(ElapsedTime) AS DATETIME), 'HH:mm:ss.fff') AS AvgTime 
FROM @Results
GROUP BY KeyType;
Run Code Online (Sandbox Code Playgroud)

这些是结果:

在此处输入图片说明

我在这里做错了什么,还是 INT 键比 25 个字符的自然键快 3 倍?

注意,我在这里写了一个后续问题。

Seb*_*ine 20

通常,SQL Server 使用B+Trees作为索引。索引查找的开销与此存储格式中键的长度直接相关。因此,代理键在索引查找上的性能通常优于自然键。

默认情况下,SQL Server 在主键上群集一个表。聚集索引键用于标识行,因此它作为包含列添加到每个其他索引中。该键越宽,每个二级索引就越大。

更糟糕的是,如果二级索引没有明确定义为UNIQUE聚集索引键,它会自动成为每个索引键的一部分。这通常适用于大多数索引,因为通常只有在要求强制唯一性时才将索引声明为唯一的。

因此,如果问题是自然与代理聚集索引,代理几乎总是会获胜。

另一方面,您将该代理列添加到表中,使表本身变大。这将导致聚集索引扫描变得更加昂贵。因此,如果您只有很少的二级索引,并且您的工作负载需要经常查看所有(或大部分)行,则实际上最好使用自然键来节省那些额外的字节。

最后,自然键通常可以更容易地理解数据模型。在使用更多存储空间的同时,自然主键会导致自然外键,从而增加本地信息密度。

因此,就像在数据库世界中一样,真正的答案是“视情况而定”。并且 - 始终使用真实数据在您自己的环境中进行测试。


Bli*_*itZ 11

我相信,最好的在于中间

自然键概述:

  1. 它们使数据模型更加明显,因为它们来自主题领域,而不是来自某人的头脑。
  2. 简单的键(一列,介于CHAR(4)和之间CHAR(20))节省了一些额外的字节,但您需要注意它们的一致性(ON UPDATE CASCADE对于那些可能会更改的键变得至关重要)。
  3. 很多情况下,当自然键很复杂时:由两列或更多列组成。如果这样的键可能作为前键迁移到另一个实体,那么它会增加数据开销(索引和数据列可能会变大)和性能下降。
  4. 如果key是一个大字符串,那么它可能总是会松散到一个整数key,因为简单的搜索条件在数据库引擎中变成了一个字节数组比较,这在大多数情况下比整数比较慢。
  5. 如果 key 是多语言字符串,则还需要查看排序规则。

好处: 1和2。

注意事项: 3、4 和 5。


人工身份密钥概述:

  1. 您无需担心它们的创建和处理(在大多数情况下),因为此功能由数据库引擎处理。默认情况下,它们是唯一的,并且不会占用太多空间。像这样的自定义操作ON UPDATE CASCADE可能会被省略,因为键值不会改变。

  2. 它们(通常)是作为外键迁移的最佳候选者,因为:

    2.1. 由一栏组成;

    2.2. 使用一个简单的类型,它具有较小的权重并且在比较操作中动作很快。

  3. 对于关联实体,哪些键不会迁移到任何地方,它可能会成为纯数据开销,因为它失去了用处。复杂的自然主键(如果那里没有字符串列)会更有用。

好处: 1和2。

注意事项: 3。


结论:

Arificial 密钥更易于维护、可靠和快速,因为它们是为此功能而设计的。但在某些情况下是不需要的。例如,CHAR(4)在大多数情况下,单列候选者的行为类似于INT IDENTITY。所以这里还有另一个问题:可维护性+稳定性还是显而易见性

问题“我是否应该注入人工密钥?” 总是取决于自然键结构:

  • 如果它包含一个大字符串,那么它会更慢,并且如果作为外部实体迁移到另一个实体,则会增加数据开销。
  • 如果它由多个列组成,那么它会更慢,并且如果作为外部实体迁移到另一个实体,则会增加数据开销。

  • *“像 ON UPDATE CASCADE 这样的自定义操作可能会被省略,因为键值不会改变。”* 代理键的作用是使每个外键引用都等同于“ON UPDATE CASCADE”。键不会改变,但它代表的值*会*。 (5认同)

nvo*_*gel 6

键是数据库的逻辑特征,而性能始终取决于存储中的物理实现以及针对该实现运行的物理操作。因此,将性能特征归因于键是错误的。

然而,在此特定示例中,将表和查询的两种可能实现相互比较。该示例没有回答此处标题中提出的问题。进行的比较是使用两种不同数据类型(整数和字符)的连接,仅使用一种类型的索引(B 树)。“显而易见”的一点是,如果使用哈希索引或其他类型的索引,则两种实现之间很可能没有可测量的性能差异。但是,该示例存在更基本的问题。

正在比较两个查询的性能,但这两个查询在逻辑上并不等效,因为它们返回不同的结果!更现实的测试将比较返回相同结果但使用不同实现的两个查询。

代理键的要点在于它是表中的一个额外属性,其中该表还具有在业务领域中使用的“有意义的”键属性。对查询结果有用的是非代理属性。因此,实际测试将仅使用自然键的表与在同一表中同时具有自然键代理键的替代实现进行比较。代理键通常需要额外的存储和索引,并且根据定义需要额外的唯一性约束。代理需要额外的处理来将外部自然键值映射到它们的代理上,反之亦然。

现在比较这个潜在的查询:

一种。

SELECT t2.NaturalTable2Key, t2.NaturalTable1Key
FROM Table2 t2;
Run Code Online (Sandbox Code Playgroud)

如果 Table2 中的 NaturalTable1Key 属性被替换为代理 IDTable1Key,则其逻辑等效项:

B.

SELECT t2.NaturalTable2Key, t1.NaturalTable1Key
FROM Table2 t2
INNER JOIN Table1 t1
ON t1.IDTable1Key = t2.IDTable1Key;
Run Code Online (Sandbox Code Playgroud)

查询 B 需要连接;查询 A 没有。这是(过度)使用代理的数据库中的常见情况。查询变得不必要地复杂并且更难优化。业务逻辑(尤其是数据完整性约束)变得更加难以实现、测试和验证。