As chaves naturais fornecem desempenho superior ou inferior no SQL Server do que as chaves inteiras substitutas?

25

Sou fã de chaves substitutas. Existe o risco de minhas descobertas serem tendenciosas na confirmação.

Muitas perguntas que eu vi aqui e em http://stackoverflow.com usam chaves naturais em vez de chaves substitutas com base em IDENTITY()valores.

Minha formação em sistemas de computadores me diz que executar qualquer operação comparativa em um número inteiro será mais rápido do que comparar seqüências de caracteres.

Esse comentário me fez questionar minhas crenças, então pensei em criar um sistema para investigar minha tese de que números inteiros são mais rápidos que cadeias de caracteres para uso como chaves no SQL Server.

Como é provável que haja muito pouca diferença discernível em conjuntos de dados pequenos, pensei imediatamente em uma configuração de duas tabelas em que a tabela principal possui 1.000.000 de linhas e a tabela secundária possui 10 linhas para cada linha da tabela primária, totalizando 10.000.000 de linhas em a tabela secundária. A premissa do meu teste é criar dois conjuntos de tabelas como este, um usando chaves naturais e outro usando chaves inteiras, e executar testes de tempo em uma consulta simples como:

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

A seguir, é o código que criei como um banco de testes:

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

O código acima cria um banco de dados e 4 tabelas e preenche as tabelas com dados, prontos para teste. O código de teste que executei é:

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;

Estes são os resultados:

insira a descrição da imagem aqui

Estou fazendo algo errado aqui ou as teclas INT são 3 vezes mais rápidas que as chaves naturais de 25 caracteres?

Observe que escrevi uma pergunta de acompanhamento aqui .

Max Vernon
fonte
11
Bem, o INT tem 4 bytes e o NVARCHAR (25) efetivo é cerca de 14 vezes mais (incluindo dados do sistema, como comprimento); portanto, apenas em termos de índice, acredito que você teria um índice PK significativamente mais amplo e profundo e, portanto, mais I / O é necessário, o que afetará o tempo de processamento. No entanto, um número inteiro natural (talvez até um dígito de verificação) seria praticamente o mesmo INT que pensamos em usar para uma coluna de identidade substituta. Portanto, a "chave natural" talvez seja INT, BIGINT, CHAR, NVARCHAR e isso importa.
RLF 29/09
7
Eu acho que o ganho de desempenho que o MikeSherrill'Catcall 'estava alcançando é que você realmente não precisa da junção na tabela "pesquisa" quando usa uma chave natural. Compare uma consulta para obter o valor da pesquisa com uma associação, com uma consulta em que o valor já esteja armazenado na tabela principal. Você pode obter um "vencedor" diferente, dependendo do comprimento da chave natural e do número de linhas na tabela de pesquisa.
Mikael Eriksson
3
O que o @MikaelEriksson disse, além dos casos em que você tem uma junção entre mais de 2 tabelas (digamos 4), onde com os substitutos você terá que juntar as tabelas A a D a B e C, enquanto que com as chaves naturais você pode unir A a D diretamente
precisa saber é o seguinte

Respostas:

18

Em geral, o SQL Server usa árvores B + para índices. A despesa de uma busca de índice está diretamente relacionada ao comprimento da chave nesse formato de armazenamento. Portanto, uma chave substituta geralmente supera uma chave natural nas buscas de índice.

O SQL Server agrupa uma tabela na chave primária por padrão. A chave de índice em cluster é usada para identificar linhas, para que seja adicionada como coluna incluída a todos os outros índices. Quanto maior a chave, maior o índice secundário.

Pior ainda, se os índices secundários não forem definidos explicitamente, UNIQUEa chave de índice em cluster se tornará automaticamente parte da chave de cada um deles. Isso geralmente se aplica à maioria dos índices, pois geralmente os índices são declarados como exclusivos apenas quando o requisito é impor exclusividade.

Portanto, se a pergunta for, índice agrupado natural versus substituto, o substituto quase sempre vencerá.

Por outro lado, você está adicionando essa coluna substituta à tabela, tornando a tabela em si maior. Isso fará com que as verificações em índice agrupadas fiquem mais caras. Portanto, se você tiver apenas muito poucos índices secundários e sua carga de trabalho precisar examinar todas as linhas (ou a maioria das) com frequência, será melhor usar uma chave natural salvando esses poucos bytes extras.

Por fim, as chaves naturais geralmente facilitam a compreensão do modelo de dados. Ao usar mais espaço de armazenamento, as chaves primárias naturais levam a chaves estrangeiras naturais que, por sua vez, aumentam a densidade de informações locais.

Assim, como tantas vezes no mundo dos bancos de dados, a resposta real é "depende". E - sempre teste em seu próprio ambiente com dados realistas.

Sebastian Meine
fonte
10

Eu acredito que o melhor está no meio .

Visão geral das chaves naturais:

  1. Eles tornam o modelo de dados mais óbvio porque são provenientes da área de assunto e não da cabeça de alguém.
  2. As chaves simples (uma coluna, entre CHAR(4)e CHAR(20)) estão economizando alguns bytes extras, mas é necessário observar a consistência ( ON UPDATE CASCADEtorna-se crítico para essas chaves, que podem ser alteradas).
  3. Muitos casos, quando as chaves naturais são complexas: consistem em duas ou mais colunas. Se essa chave puder migrar para outra entidade como uma chave estrangeira, ela adicionará uma sobrecarga de dados (os índices e as colunas de dados podem se tornar grandes) e o desempenho será fraco.
  4. Se key for uma string grande, provavelmente sempre será perdida para uma chave inteira, porque a simples condição de pesquisa se torna uma comparação de matriz de bytes em um mecanismo de banco de dados, que na maioria dos casos é mais lento que a comparação inteira.
  5. Se key for uma cadeia de caracteres multilíngüe, também será necessário assistir aos agrupamentos.

Benefícios: 1 e 2.

Observações: 3, 4 e 5.


Visão geral das chaves de identidade artificial:

  1. Você não precisa se preocupar com a criação e o manuseio (na maioria dos casos), pois esse recurso é tratado pelo mecanismo de banco de dados. Eles são únicos por padrão e não ocupam muito espaço. Operações personalizadas como ON UPDATE CASCADEpodem ser omitidas, porque os valores-chave não mudam.

  2. Eles (geralmente) são os melhores candidatos à migração como chaves estrangeiras porque:

    2.1 consiste em uma coluna;

    2.2 usando um tipo simples que tenha um peso pequeno e atue rapidamente para operações de comparação.

  3. Para uma entidade de associação, cujas chaves não são migradas para lugar algum, ela pode se tornar uma sobrecarga de dados pura, pois a utilidade é perdida. Chave primária natural complexa (se não houver colunas de cadeia de caracteres) será mais útil.

Benefícios: 1 e 2.

Observações: 3.


CONCLUSÃO:

As chaves artificiais são mais fáceis de manter, confiáveis ​​e rápidas, porque foram projetadas para esses recursos. Mas em alguns casos não são necessários. Por exemplo, o CHAR(4)candidato a coluna única na maioria dos casos se comporta como INT IDENTITY. Portanto, há outra questão aqui também: manutenção + estabilidade ou obviedade ?

Pergunta "Devo injetar uma chave artificial ou não?" depende sempre da estrutura natural da chave:

  • Se ele contiver uma cadeia grande, será mais lenta e adicionará uma sobrecarga de dados ao migrar como externa para outra entidade.
  • Se consistir em várias colunas, será mais lento e adicionará uma sobrecarga de dados ao migrar como estrangeiro para outra entidade.
BlitZ
fonte
5
"Operações personalizadas como ON UPDATE CASCADE podem ser omitidas, porque os valores-chave não mudam." O efeito das chaves substitutas é tornar cada referência de chave estrangeira equivalente a "ON UPDATE CASCADE". A chave não muda, mas o valor que representa o faz .
Mike Sherrill 'Cat Recall'
@ MikeSherrill'Catcall 'Sim, é claro. No entanto, ON UPDATE CASCADEnão é usado, enquanto as chaves nunca foram atualizadas. Mas, se estiverem, pode ser um problema se ON UPDATE NO ACTIONestiver configurado. Quero dizer, que o DBMS nunca o use, enquanto os valores da coluna-chave não foram alterados.
BlitZ
4

Uma chave é um recurso lógico de um banco de dados, enquanto o desempenho é sempre determinado pela implementação física no armazenamento e pelas operações físicas executadas nessa implementação. Portanto, é um erro atribuir características de desempenho às chaves.

Neste exemplo em particular, no entanto, duas implementações possíveis de tabelas e consultas são comparadas entre si. O exemplo não responde à pergunta que está sendo colocada no título aqui. A comparação que está sendo feita é de junções usando dois tipos de dados diferentes (inteiro e caractere) usando apenas um tipo de índice (árvore B). Um ponto "óbvio" é que, se um índice de hash ou outro tipo de índice fosse usado, possivelmente não haveria diferença mensurável de desempenho entre as duas implementações. No entanto, existem problemas mais fundamentais com o exemplo.

Duas consultas estão sendo comparadas quanto ao desempenho, mas as duas consultas não são logicamente equivalentes porque retornam resultados diferentes! Um teste mais realista compararia duas consultas retornando os mesmos resultados, mas usando implementações diferentes.

O ponto essencial sobre uma chave substituta é que ela é um atributo extra em uma tabela em que a tabela também possui atributos de chave "significativos" usados ​​no domínio de negócios. São os atributos não substitutos que são de interesse para que os resultados da consulta sejam úteis. Portanto, um teste realista compararia as tabelas usando apenas chaves naturais com uma implementação alternativa com chaves naturais e substitutas na mesma tabela. As chaves substitutas normalmente requerem armazenamento e indexação adicionais e, por definição, exigem restrições adicionais de exclusividade. Os substitutos requerem processamento adicional para mapear os valores da chave natural externa em seus substitutos e vice-versa.

Agora compare esta consulta em potencial:

UMA.

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

Para seu equivalente lógico, se o atributo NaturalTable1Key na Tabela2 for substituído pelo IDTable1Key substituto:

B.

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

A consulta B requer uma associação; A consulta A não. Essa é uma situação familiar em bancos de dados que usam (em excesso) substitutos. As consultas se tornam desnecessariamente complexas e muito mais difíceis de otimizar. A lógica comercial (especialmente as restrições de integridade de dados) se torna mais difícil de implementar, testar e verificar.

nvogel
fonte