Posts tagged klucze obce

[PL] SQL Server – Indeksy na kolumnach kluczy obcych

VN:F [1.7.9_1023]
Rating: 5.0/5 (1 vote cast)

Tytułem wstępu

Jedną z najlepszych praktyk w relacyjnych bazach danych jest utworzenie indeksów na kolumnach, które wchodzą w skład kluczy obcych. Kolumny takie są często wykorzystywane do łączenia tabel w zapytaniach. Oczywiście nie należy bezwarunkowo zakładać indeksu na każdej kombinacji kolumn, tworzącej klucz obcy, ale przynajmniej należy taką możliwość rozpatrzyć (decyzja zależy również od rozkładu danych).

SQL Server nie tworzy takich indeksów samodzielnie (niektóre systemy potrafią zakładać indeksy automatycznie przy tworzeniu kluczy obcych). Celem niniejszego artykułu jest pokazanie na przykładach, dlaczego indeksowanie kolumn kluczy obcych może być dobrym pomysłem.

Zakładam, że wiesz, czym jest indeks i do czego służy. Jeżeli jest inaczej, zapoznaj się najpierw z tematyką indeksów i wróć do tego wpisu, gdy opanujesz niezbędne podstawy.
Dziękuję Pani Ewie Nowickiej, współpracującej z portalem WSS.pl, za korektę niniejszego tekstu.

Tworzymy poligon

Posłużę się klasycznym przykładem dwóch tabel: tabeli produktów i tabeli kategorii produktów. Tabele są powiązane związkiem binarnym jeden do wielu (jedna kategoria może mieć wiele produktów). W tabeli dbo.Product (zbiór produktów) pojawia się kolumna CategoryID, wskazująca na odpowiednią kategorię w tabeli dbo.Category. Kod do wygenerowania bazy danych i jej zawartości:

CREATE DATABASE Test;
GO
USE Test;
GO
CREATE TABLE dbo.Category (
  CategoryID int NOT NULL IDENTITY(1,1) PRIMARY KEY NONCLUSTERED,
  CategoryName varchar(50) NOT NULL UNIQUE CLUSTERED
);
GO
CREATE TABLE dbo.Product (
  ProductID int NOT NULL IDENTITY(1,1) PRIMARY KEY NONCLUSTERED,
  ProductName varchar(50) NOT NULL UNIQUE CLUSTERED,
  CategoryID int NULL
);
GO
ALTER TABLE dbo.Product
ADD CONSTRAINT FK_Product_Category
FOREIGN KEY (CategoryID)
REFERENCES dbo.Category (CategoryID);
GO
-- Generujemy 1000 rekordów kategorii produktów
INSERT INTO dbo.Category (CategoryName)
SELECT 'Category ' + CONVERT(varchar(4), number)
FROM master.dbo.spt_values
WHERE type = 'P' AND number BETWEEN 1 AND 1000;
GO
-- Generujemy około 248 tys. rekordów produktów
INSERT INTO dbo.Product (ProductName, CategoryID)
SELECT
  'Product ' + CONVERT(varchar(7), Q.n),
  CASE WHEN Q.n % 1000 = 0 THEN 1 ELSE Q.n % 1000 END
FROM (
  SELECT DISTINCT v1.number * v2.number AS n
  FROM master.dbo.spt_values AS v1
  CROSS JOIN master.dbo.spt_values AS v2
  WHERE v1.number BETWEEN 1 AND 1000 AND v1.type = 'P'
  AND v2.number BETWEEN 1 AND 1000 AND v2.type = 'P'
) AS Q
ORDER BY Q.n;
GO

Przypadek 1. – SELECT

Rozpatrzmy następujące zapytanie SELECT:

SELECT c.CategoryName, p.ProductName
FROM dbo.Category AS c
LEFT JOIN dbo.Product AS p
ON c.CategoryID = p.CategoryID
WHERE c.CategoryName = 'Category 1000';

Ma ono za zadanie wybranie wszystkich produktów należących do kategorii o nazwie „Category 1000” (a jeżeli do tej kategorii nie należy żaden produkt, powinien pojawić się jeden rekord z pustą nazwą produktu). Takich produktów w tabeli dbo.Product nie ma. Ale mimo to zapytanie wykonywane jest w zauważalnie długim czasie, ponieważ brak indeksu na kolumnie CategoryID powoduje, że musi nastąpić przeskanowanie (odczyt wszystkich rekordów) tabeli dbo.Product. Plan zapytania wygląda następująco:

clip_image001[7]

Po założeniu, za pomocą poniższego kodu, indeksu na kolumnie CategoryID, w planie nie widzimy już skanowania tabeli produktów wykonywanego w pętli, a samo zapytanie wykonuje się błyskawicznie.

CREATE INDEX IX_Product_CategoryID
ON dbo.Product (CategoryID);

clip_image002[7]

Przypadek 2. – UPDATE

Ale indeks może posłużyć nie tylko do optymalizacji zapytań SELECT. Dzięki niemu można także spowodować, że operacje modyfikacji danych będą wykonywać się szybciej. Weźmy takie polecenie UPDATE:

UPDATE p
SET p.CategoryID = 1
FROM dbo.Category AS c
INNER JOIN dbo.Product AS p
ON c.CategoryID = p.CategoryID
WHERE c.CategoryName = 'Category 1000';

Celem jest przypisanie produktów, dotąd należących do „Category 1000”, do nowej kategorii o identyfikatorze 1. Jeśli na kolumnie CategoryID w tabeli dbo.Product brak indeksu, przy wykynywaniu powyższego polecenia UPDATE praktycznie cały koszt jest generowany przez skanowanie tabeli dbo.Product oraz operatora złączenia typu Nested Loops.

clip_image003[7]

Ale jeżeli dodany zostanie indeks (taki sam, jak w przypadku 1.), skanowanie zastępowane jest przez operację jego przeszukania. Sama modyfikacja danych będzie wykonywana szybciej, na skutek szybszego znalezienia rekordów do zmiany. Odpowiedni fragment planu zapytania po założeniu indeksu:

clip_image004[7]

Przypadek 3. – DELETE

Także operacje kasowania danych mogą być optymalizowane przez założenie indeksu na kolumnie, będącej kluczem obcym. Przykładowe polecenie DELETE:

DELETE FROM dbo.Category
WHERE CategoryName = 'Category 1000';

W planie wykonania, w przypadku braku indeksu na kolumnie CategoryID w tabeli dbo.Product, pojawia się skanowanie całej tabeli w poszukiwaniu rekordów produktów, należących do „Category 1000” (nawet, jeśli takich produktów nie ma, przejrzana zostaje cała tabela). Jest to spowodowane tym, że w tabeli dbo.Product istnieje klucz obcy, odwołujący się do tabeli dbo.Category i uniemożliwiający usuwanie rekordu kategorii, jeżeli ma ona przypisany choć jeden produkt. clip_image006[7]

Jakie korzyści daje założenie indeksu na kolumnie (-ach) klucza obcego w tabeli dbo.Product (takiego samego indeksu, jak w przypadkach 1. i 2.)?

clip_image007[7]

Tym razem operacja wykonuje się szybciej, ponieważ zamiast skanowania tabeli dbo.Product następuje przeszukanie indeksu na kolumnie CategoryID w tej tabeli.

Wnioski daleko idące

Nie zawsze indeksy na kolumnach, będących kluczami obcymi, pozwalają na optymalizację zapytań. Ale na pewno kolumny takie powinny być uwzględniane jako propozycje kluczy indeksów. Przykłady z tego artykułu pokazują, że takim indeksem można także przyspieszyć modyfikacje danych (choć spotyka się ogólne i niezbyt trafne stwierdzenia, że indeksy zawsze powodują spowolnienie takich operacji).

[PL] Skryptowanie w SQL Server 2008 – Klucze obce

VN:F [1.7.9_1023]
Rating: 0.0/5 (0 votes cast)

Intro

To już trzeci wpis z serii “Skryptowanie w SQL Server 2008″. Dwa poprzednie dostępne są tu:

Tym razem przedstawiam sposób na skryptowanie kluczy obcych. Kiedy takie skryptowanie może się przydać? Na przykład wtedy, gdy chcemy usunąć, a następnie odtworzyć owe klucze (typowy scenariusz: import wyczyszczonych i zwalidowanych danych). W podsumowaniu podam, jakie rozwinięcia mojego kodu można zaimplementować, by za pomocą praktycznie tego samego kodu wykonywać inne zadania związane z kluczami obcymi.

Skryptowanie kluczy obcych

Zadanie: zeskryptować wszystkie klucze obce w bazie danych.

Rozwiązanie:

USE AdventureWorks2008R2;
GO
SET NOCOUNT ON;

-- tu będziemy trzymać generowany kod
DECLARE @SQL TABLE (
  LineId int IDENTITY(1,1) NOT NULL PRIMARY KEY,
  Line nvarchar(4000) NOT NULL
);

DECLARE
  @object_name sysname,
  @object_id int,
  @object_schema_name sysname,
  @parent_object_name sysname,
  @parent_object_schema_name sysname,
  @is_not_for_replication bit,
  @delete_referential_action int,
  @update_referential_action int,
  @referenced_object_schema_name sysname,
  @referenced_object_name sysname,
  @column_name sysname,
  @referenced_column_name sysname,
  @columns nvarchar(4000),
  @referenced_columns nvarchar(4000);

-- kursor do wyciągania pojedynczych kluczy obcych
DECLARE CursorFK CURSOR LOCAL STATIC READ_ONLY FORWARD_ONLY
FOR
  SELECT
    name,
    [object_id],
    SCHEMA_NAME([schema_id]),
    OBJECT_SCHEMA_NAME(parent_object_id),
    OBJECT_NAME(parent_object_id),
    OBJECT_SCHEMA_NAME(referenced_object_id),
    OBJECT_NAME(referenced_object_id),
    is_not_for_replication,
    delete_referential_action,
    update_referential_action
  FROM sys.foreign_keys
  WHERE is_ms_shipped = 0;
OPEN CursorFK;
FETCH NEXT FROM CursorFK
INTO @object_name, @object_id, @object_schema_name,
     @parent_object_schema_name, @parent_object_name,
     @referenced_object_schema_name, @referenced_object_name,
     @is_not_for_replication,
     @delete_referential_action, @update_referential_action;

WHILE @@FETCH_STATUS = 0 BEGIN
  SELECT @columns = '', @referenced_columns = '';

  -- generujemy sprawdzenie, czy klucz nie istnieje
  INSERT INTO @SQL (Line)
  SELECT 'IF OBJECT_ID(N''' +
        REPLACE(
          QUOTENAME(@object_schema_name) + '.' +
          QUOTENAME(@object_name), '''', ''''''
        ) + ''', N''F'') IS NULL';

  -- generujemy polecenie ALTER TABLE ... ADD CONSTRAINT
  INSERT INTO @SQL (Line)
  SELECT '  ALTER TABLE ' +
    QUOTENAME(@parent_object_schema_name) + '.' +
    QUOTENAME(@parent_object_name);
  INSERT INTO @SQL (Line)
  SELECT '  ADD CONSTRAINT ' + QUOTENAME(@object_name);

  -- kursor do wyciągania kolejnych kolumn klucza obcego
  DECLARE CursorFKColumns CURSOR LOCAL STATIC READ_ONLY FORWARD_ONLY
  FOR
    SELECT
      pc.name,
      rc.name
    FROM sys.foreign_key_columns AS f
    INNER JOIN sys.columns AS pc
    ON f.parent_object_id = pc.[object_id]
    AND f.parent_column_id = pc.column_id
    INNER JOIN sys.columns AS rc
    ON f.referenced_object_id = rc.[object_id]
    AND f.referenced_column_id = rc.column_id
    WHERE f.constraint_object_id = @object_id
    ORDER BY f.constraint_column_id
  OPEN CursorFKColumns;
  FETCH NEXT FROM CursorFKColumns
  INTO
    @column_name,
    @referenced_column_name;
  WHILE @@FETCH_STATUS = 0 BEGIN
    -- doklejamy do zestawów kolumn
    SET @columns = @columns + ', ' + QUOTENAME(@column_name);
    SET @referenced_columns = @referenced_columns + ', ' + QUOTENAME(@referenced_column_name);

    FETCH NEXT FROM CursorFKColumns
    INTO
      @column_name,
      @referenced_column_name;
  END;
  CLOSE CursorFKColumns;
  DEALLOCATE CursorFKColumns;

  -- generujemy kod z listami kolumn
  INSERT INTO @SQL (Line)
  SELECT '  FOREIGN KEY (' + STUFF(@columns, 1, 2, '') + ')';
  INSERT INTO @SQL (Line)
  SELECT '  REFERENCES ' +
    QUOTENAME(@referenced_object_schema_name) + '.' +
    QUOTENAME(@referenced_object_name) + '(' +
    STUFF(@referenced_columns, 1, 2, '') + ')';

  -- akcje klucza przy usuwaniu wierszy
  IF @delete_referential_action <> 0 BEGIN
    INSERT INTO @SQL (Line)
    SELECT '  ON DELETE ' +
      CASE @delete_referential_action
        WHEN 1 THEN 'CASCADE'
        WHEN 2 THEN 'SET NULL'
        WHEN 3 THEN 'SET DEFAULT'
      END;
  END;

  -- akcje klucza przy modyfikacji wierszy
  IF @update_referential_action <> 0 BEGIN
    INSERT INTO @SQL (Line)
    SELECT '  ON UPDATE ' +
      CASE @update_referential_action
        WHEN 1 THEN 'CASCADE'
        WHEN 2 THEN 'SET NULL'
        WHEN 3 THEN 'SET DEFAULT'
      END;
  END;

  -- czy wymuszamy regułę klucza w replikacjach
  IF @is_not_for_replication = 1
    INSERT INTO @SQL (Line)
    SELECT '  NOT FOR REPLICATION';

  UPDATE @SQL
  SET Line = Line + ';'
  WHERE LineId = SCOPE_IDENTITY();

  INSERT INTO @SQL (Line) SELECT 'GO';

  FETCH NEXT FROM CursorFK
  INTO @object_name, @object_id, @object_schema_name,
     @parent_object_schema_name, @parent_object_name,
     @referenced_object_schema_name, @referenced_object_name,
     @is_not_for_replication,
     @delete_referential_action, @update_referential_action;
END;
CLOSE CursorFK;
DEALLOCATE CursorFK;
SELECT Line AS ' ' FROM @SQL ORDER BY LineId;
GO

Fragment wyniku:

Uwagi: w Management Studio ustaw Results to Text, sugeruję też zwiększenie maksymalnej długości tekstu w jednej kolumnie zwracanej w wynikach zapytań (w menu głównym kliknij Tools – Options – Query Results – SQL Server – Results to Text i w polu Maximum number of characters displayed in each column wpisz 8192, a następnie uruchom ponownie Management Studio).

Komentarz do kodu:

  • metadane kluczy obcych zwracają widoki sys.foreign_keys (metadane tabeli, w której tworzymy klucz, opcje dla poleceń DELETE / UPDATE, opcja wyłączenia klucza dla replikacji) oraz sys.foreign_key_columns (metadane kolumn oraz tabeli, do której odwołuje się klucz obcy),
  • funkcja QUOTENAME jak zwykle służy mi do otaczania identyfikatorów nawiasami kwadratowymi i apostrofami,
  • fragmenty generowanego skryptu T-SQL są dodawane w kursorze (iteracja = definicja jednego obiektu) do zmiennej tabelarycznej @SQL (linie są numerowane autonumeracją w kolumnie LineId, zakładam maksymalną długość jednej linii kodu w obiekcie na 4000 znaków),
  • rozwiązanie oparłem o dwa kursory: CursorFK – do iterowania po kluczach obcych oraz zagnieżdżony CursorFKColumns – do iterowania po kolumnach bieżącego klucza,
  • ponieważ nie jestem zwolennikiem wyłączania więzów, moje rozwiązanie nie uwzględnia kolumny is_disabled z widoku sys.foreign_keys (nie generuję klauzuli WITH NOCHECK nawet, gdy klucz jest wyłączony).

Słowo podsumowania

Powyższy kod można rozwinąć:

  • można dołożyć filtrowanie po tabelach (czy to tabeli, w której klucz jest utworzony, czy po tabeli, do której klucz się odwołuje),
  • można generować tylko składnie ALTER TABLE … DROP CONSTRAINT, jeżeli zależy nam tylko na usunięciu wybranych kluczy,
  • można wykorzystać kolumnę is_system_named z widoku sys.foreign_keys, jeżeli chcemy odnaleźć te klucze, które otrzymały nazwy wygenerowane systemowo (jeżeli nie chcemy mieć w bazie danych obiektów nazywanych dość przypadkowo).

Na koniec informacja – w repozytorium kodu umieściłem kod pseudo-systemowej procedury sp_dropforeignkeys, która może posłużyć do wygenerowania kodu usuwającego klucze obce wskazujące na wybraną tabelę. W procedurze tej wykorzystałem te same obiekty systemowe, co w powyższym kodzie, więc komentarz do niej wydaje mi się zbędny. Po stworzeniu w bazie master i oznaczeniu jako obiekt systemowy procedura powinna działać w kontekście dowolnej bazy danych.