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


25

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

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

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

评论使我质疑我的信念,因此我认为我将创建一个系统来研究我的论点,即在SQL Server中用作键的整数比字符串快。

由于小型数据集之间的区别可能很小,因此我立即想到了两个表的设置,其中主表具有1,000,000行,而辅助表在主表中的每一行有10行,总共有10,000,000行。辅助表。我的测试的前提是创建两组这样的表,一组使用自然键,一组使用整数键,然后对一个简单的查询运行计时测试,例如:

SELECT *
FROM Table1
    INNER JOIN Table2 ON Table1.Key = Table2.Key;

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

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

上面的代码创建一个数据库和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;

结果如下:

在此处输入图片说明

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

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


1
那么INT是4个字节,有效的NVARCHAR(25)大约长14倍(包括长度之类的系统数据),所以仅就索引而言,我相信您的PK索引会更广,更深,因此需要/ O,这会影响处理时间。Howevev的自然整数(甚至可能是校验位)与我们认为用于代理Identity列的INT几乎相同。因此,“自然键”可能是INT,BIGINT,CHAR,NVARCHAR,而所有这些都很重要。
RLF

7
我认为@ MikeSherrill'Catcall'获得的性能提升是,当您使用自然键时,您实际上不需要针对“ lookup”表的联接。将查询与具有联接的查询值进行比较,将查询与已将值存储在主表中的查询进行比较。您可能会获得不同的“优胜者”,具体取决于自然键长度和查找表中的行数。
Mikael Eriksson 2013年

3
@MikaelEriksson所说的内容以及在两个以上的表之间进行联接(例如4)的情况下,其中必须使用代理将表A到D通过B和C联接起来,而使用自然键可以将A到D直接联接
ypercubeᵀᴹ

Answers:


18

通常,SQL Server使用B + Trees作为索引。索引查找的开销与该存储格式中密钥的长度直接相关。因此,代理键通常胜过索引搜索的自然键。

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

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

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

另一方面,您要将该替代列添加到表中,从而使表本身更大。这将导致聚集索引扫描变得更加昂贵。因此,如果您只有很少的二级索引,并且您的工作量需要经常查看所有(或大多数)行,那么自然键可能会更好,因为可以节省一些额外的字节。

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

因此,在数据库世界中,真正的答案是“取决于”。并且-始终在您自己的环境中使用真实数据进行测试。


10

我相信,最好的在于中间

自然键概述:

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

优势: 1和2。

监视数: 3、4和5。


人工身份密钥概述:

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

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

    2.1。由一栏组成;

    2.2。使用重量轻且比较操作快速的简单类型。

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

优势: 1和2。

监视: 3。


结论:

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

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

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

5
“可能会省略诸如ON UPDATE CASCADE之类的自定义操作,因为键值不会更改。” 代理键的作用是使每个外键引用都等效于“ ON UPDATE CASCADE”。关键不改变,但它代表的价值
Mike Sherrill'Cat Recall'13

@ MikeSherrill'Catcall'是的,当然。但是,ON UPDATE CASCADE没有使用过,而密钥从未更新过。但是,如果是这样,那么如果ON UPDATE NO ACTION进行了配置,则可能是一个问题。我的意思是,DBMS从未使用过它,而键列的值没有改变。
BlitZ

4

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

但是,在此特定示例中,将表和查询的两种可能的实现方式进行了比较。该示例未回答此处标题中提出的问题。进行的比较是使用仅使用一种索引(B树)的两种不同数据类型(整数和字符)的联接。一个“明显”的观点是,如果使用哈希索引或其他类型的索引,则这两种实现之间的性能差异可能会很明显。但是,该示例还有更多基本问题。

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

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

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

一种。

SELECT t2.NaturalTable2Key, t2.NaturalTable1Key
FROM Table2 t2;

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

B.

SELECT t2.NaturalTable2Key, t1.NaturalTable1Key
FROM Table2 t2
INNER JOIN Table1 t1
ON t1.IDTable1Key = t2.IDTable1Key;

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.