Czy klucze naturalne zapewniają wyższą lub niższą wydajność w SQL Server niż zastępcze klucze całkowite?

25

Jestem fanem zastępczych kluczy. Istnieje ryzyko, że moje ustalenia są stronnicze.

Wiele pytań, które widziałem zarówno tutaj, jak i na stronie http://stackoverflow.com, używa kluczy naturalnych zamiast kluczy zastępczych opartych na IDENTITY()wartościach.

Moje doświadczenie w systemach komputerowych mówi mi, że wykonanie dowolnej operacji porównawczej na liczbie całkowitej będzie szybsze niż porównywanie ciągów.

Ten komentarz skłonił mnie do zakwestionowania moich przekonań, więc pomyślałem, że stworzę system do zbadania mojej tezy, że liczby całkowite są szybsze niż ciągi znaków do użycia jako klucze w SQL Server.

Ponieważ istnieje prawdopodobieństwo bardzo niewielkiej różnicy w małych zestawach danych, od razu pomyślałem o konfiguracji dwóch tabel, w której tabela podstawowa ma 1 000 000 wierszy, a tabela dodatkowa ma 10 wierszy dla każdego wiersza w tabeli podstawowej, co daje łącznie 10 000 000 wierszy drugi stół. Założeniem mojego testu jest utworzenie dwóch takich zestawów tabel, jednego z wykorzystaniem kluczy naturalnych i drugiego z użyciem kluczy całkowitych, i uruchomienia testów czasowych na prostym zapytaniu, takim jak:

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

Oto kod, który utworzyłem jako łóżko testowe:

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

Powyższy kod tworzy bazę danych i 4 tabele oraz wypełnia tabele danymi, gotowe do przetestowania. Uruchomiony przeze mnie kod testowy to:

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;

Oto wyniki:

wprowadź opis zdjęcia tutaj

Czy robię tutaj coś złego, czy klucze INT są 3 razy szybsze niż naturalne klawisze 25 znaków?

Uwaga, napisałem tutaj pytanie uzupełniające .

Max Vernon
źródło
1
Cóż, INT ma 4 bajty, a efektywny NVARCHAR (25) jest około 14 razy dłuższy (w tym dane systemowe, takie jak długość), więc pod względem samego indeksu uważam, że miałbyś znacznie szerszy i głębszy indeks PK, a zatem więcej I / O jest potrzebne, co wpłynie na czas przetwarzania. Jednak naturalną liczbą całkowitą (być może nawet cyfrą kontrolną) byłaby właściwie ta sama INT, którą myślimy o użyciu w zastępczej kolumnie Tożsamości. Zatem „naturalny klucz” może być INT, BIGINT, CHAR, NVARCHAR i to wszystko ma znaczenie.
RLF
7
Myślę, że wzrost wydajności @ MikeSherrill'Catcall 'osiągał to, że tak naprawdę nie potrzebujesz łączenia z tabelą „odnośników”, kiedy używasz naturalnego klucza. Porównaj zapytanie, aby uzyskać wartość wyszukiwania z łączeniem, z zapytaniem, w którym wartość jest już zapisana w głównej tabeli. Możesz otrzymać innego „zwycięzcę” w zależności od naturalnej długości klucza i liczby wierszy w tabeli odnośników.
Mikael Eriksson
3
To, co powiedział @MikaelEriksson, plus przypadki, gdy masz połączenie między więcej niż 2 tabelami (powiedzmy 4), gdzie z surogatami będziesz musiał dołączyć do tabel od A do D do B i C, podczas gdy z naturalnymi kluczami możesz bezpośrednio dołączyć A do D
ypercubeᵀᴹ

Odpowiedzi:

18

Ogólnie rzecz biorąc, SQL Server używa drzew B + do indeksów. Koszt wyszukiwania indeksu jest bezpośrednio związany z długością klucza w tym formacie pamięci. Dlatego klucz zastępczy zwykle przewyższa naturalny klucz przy wyszukiwaniu indeksu.

SQL Server domyślnie klastruje tabelę na kluczu podstawowym. Klastrowy klucz indeksu służy do identyfikowania wierszy, więc jest dodawany jako kolumna dołączona do każdego innego indeksu. Im szerszy ten klucz, tym większy jest każdy indeks wtórny.

Co gorsza, jeśli indeksy drugorzędne nie są wyraźnie zdefiniowane, ponieważ UNIQUEklastrowany klucz indeksu automatycznie staje się częścią klucza każdego z nich. Zwykle dotyczy to większości indeksów, ponieważ zwykle indeksy są deklarowane jako unikalne tylko wtedy, gdy wymaga się wymuszenia unikatowości.

Jeśli więc pytanie brzmi: indeks naturalny w porównaniu do klastrowanego surogatu, surogat prawie zawsze wygrywa.

Z drugiej strony dodajesz tę tabelę zastępczą do tabeli, dzięki czemu sama tabela jest większa. Spowoduje to, że skanowanie indeksów klastrowych będzie droższe. Tak więc, jeśli masz tylko bardzo niewiele indeksów wtórnych, a twoje obciążenie wymaga częstego przeglądania wszystkich (lub większości) wierszy, możesz być lepszy z naturalnym kluczem oszczędzającym te dodatkowe bajty.

Wreszcie, naturalne klucze często ułatwiają zrozumienie modelu danych. Przy użyciu większej przestrzeni dyskowej naturalne klucze podstawowe prowadzą do naturalnych kluczy obcych, co z kolei zwiększa gęstość lokalnych informacji.

Tak więc, jak to często bywa w świecie baz danych, prawdziwą odpowiedzią jest „to zależy”. I - zawsze testuj we własnym środowisku z realistycznymi danymi.

Sebastian Meine
źródło
10

Uważam, że najlepsze leży w środku .

Omówienie kluczy naturalnych:

  1. Sprawiają, że model danych staje się bardziej oczywisty, ponieważ pochodzą z obszaru tematycznego, a nie z czyjejś głowy.
  2. Proste klucze (jedna kolumna, pomiędzy CHAR(4)i CHAR(20)) zapisują dodatkowe bajty, ale musisz uważać na ich spójność ( ON UPDATE CASCADEstaje się krytyczna dla tych kluczy, które można zmienić).
  3. Wiele przypadków, gdy klucze naturalne są złożone: składa się z dwóch lub więcej kolumn. Jeśli taki klucz może migrować do innej jednostki jako klucz prognostyczny, spowoduje to dodanie narzutu danych (indeksy i kolumny danych mogą stać się duże) i obniżenie wydajności.
  4. Jeśli klucz jest dużym łańcuchem, to prawdopodobnie zawsze będzie luźny do klucza liczb całkowitych, ponieważ prosty warunek wyszukiwania staje się porównaniem tablicy bajtów w silniku bazy danych, który w większości przypadków jest wolniejszy niż porównanie liczb całkowitych.
  5. Jeśli klucz jest ciągiem wielojęzycznym, musisz również obejrzeć zestawienia.

Korzyści: 1 i 2.

Obserwacje: 3, 4 i 5.


Omówienie kluczy sztucznej tożsamości:

  1. Nie trzeba przejmować się ich tworzeniem i obsługą (w większości przypadków), ponieważ ta funkcja jest obsługiwana przez silnik bazy danych. Są domyślnie unikalne i nie zajmują dużo miejsca. Niestandardowe operacje, takie jak, ON UPDATE CASCADEmogą zostać pominięte, ponieważ kluczowe wartości nie ulegają zmianie.

  2. Są (często) najlepszymi kandydatami do migracji jako klucze obce, ponieważ:

    2.1 składa się z jednej kolumny;

    2.2 za pomocą prostego typu, który ma niewielką wagę i działa szybko podczas operacji porównania.

  3. W przypadku jednostek asocjacyjnych, których klucze nie są nigdzie migrowane, może stać się czystym narzutem danych, ponieważ jego użyteczność zostanie utracona. Złożony naturalny klucz główny (jeśli nie ma tam kolumn ciągów) będzie bardziej przydatny.

Korzyści: 1 i 2.

Obserwacje: 3.


WNIOSEK:

Klucze Arificial są łatwiejsze w utrzymaniu, niezawodne i szybkie, ponieważ zostały zaprojektowane dla tych funkcji. Ale w niektórych przypadkach nie są potrzebne. Na przykład CHAR(4)kandydat na jedną kolumnę w większości przypadków zachowuje się podobnie INT IDENTITY. Jest więc także inne pytanie: łatwość konserwacji + stabilność czy oczywistość ?

Pytanie „Czy powinienem wstrzyknąć sztuczny klucz, czy nie?” zawsze zależy od naturalnej struktury klucza:

  • Jeśli zawiera duży ciąg, wówczas jest wolniejszy i spowoduje narzut danych, jeśli migruje jako obcy do innej jednostki.
  • Jeśli składa się z wielu kolumn, jest wolniejszy i spowoduje zwiększenie danych w przypadku migracji jako obcej do innej jednostki.
Ciężkie bombardowanie
źródło
5
„Niestandardowe operacje, takie jak ON UPDATE CASCADE, mogą zostać pominięte, ponieważ kluczowe wartości nie ulegają zmianie.” Działanie kluczy zastępczych polega na tym, aby każde odniesienie do klucza obcego było równoważne z „ON UPDATE CASCADE”. Klucz się nie zmienia, ale wartość, którą reprezentuje, zmienia się .
Mike Sherrill „Cat Recall”
@ MikeSherrill'Catcall 'Tak, oczywiście. Jednak ON UPDATE CASCADEnie używany, a klucze nigdy nie były aktualizowane. Ale jeśli tak, to może być problem, jeśli ON UPDATE NO ACTIONjest skonfigurowany. Mam na myśli, że DBMS nigdy go nie używa, podczas gdy wartości kluczowych kolumn nie ulegają zmianie.
BlitZ
4

Klucz jest logiczną funkcją bazy danych, podczas gdy wydajność zawsze zależy od fizycznej implementacji w pamięci masowej i operacji fizycznych wykonywanych na tej implementacji. Dlatego błędem jest przypisywanie kluczom charakterystyki wydajności.

W tym konkretnym przykładzie porównano jednak dwie możliwe implementacje tabel i zapytań. Przykład nie odpowiada na pytanie postawione w tytule tutaj. Porównywane są sprzężenia przy użyciu dwóch różnych typów danych (liczba całkowita i znak) przy użyciu tylko jednego typu indeksu (B-drzewo). „Oczywistym” punktem jest to, że gdyby zastosowano indeks skrótu lub inny typ indeksu, prawdopodobnie nie byłoby mierzalnej różnicy wydajności między tymi dwoma implementacjami. Istnieją jednak bardziej podstawowe problemy z przykładem.

Dwa zapytania są porównywane pod kątem wydajności, ale dwa zapytania nie są logicznie równoważne, ponieważ zwracają różne wyniki! Bardziej realistyczny test porównałby dwa zapytania zwracające te same wyniki, ale wykorzystujące różne implementacje.

Istotną kwestią dotyczącą klucza zastępczego jest to, że jest to dodatkowy atrybut w tabeli, w której tabela ma również „znaczące” atrybuty kluczowe używane w domenie biznesowej. Przydatne są atrybuty niebędące odpowiednikami. Realistyczny test porównałby zatem tabele przy użyciu tylko naturalnych kluczy z alternatywną implementacją zawierającą zarówno klucze naturalne, jak i zastępcze w tej samej tabeli. Klucze zastępcze zazwyczaj wymagają dodatkowego przechowywania i indeksowania, a z definicji wymagają dodatkowych ograniczeń unikatowości. Surogaty wymagają dodatkowego przetwarzania, aby zmapować zewnętrzne wartości klucza naturalnego na ich surogaty i odwrotnie.

Teraz porównaj to potencjalne zapytanie:

ZA.

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

Do jego logicznego odpowiednika, jeśli atrybut NaturalTable1Key w tabeli 2 zostanie zastąpiony zastępczym IDTable1Key:

B.

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

Zapytanie B wymaga sprzężenia; Zapytanie A nie. Jest to znana sytuacja w bazach danych, które (nad) używają parametrów zastępczych. Zapytania stają się niepotrzebnie złożone i trudniejsze do optymalizacji. Logika biznesowa (zwłaszcza ograniczenia integralności danych) staje się trudniejsza do wdrożenia, przetestowania i weryfikacji.

nvogel
źródło