Comment SQL Server choisit-il une clé d'index pour une référence de clé étrangère?

9

Je travaille avec une base de données héritée qui a été importée de MS Access. Il existe une vingtaine de tables avec des clés primaires uniques non groupées qui ont été créées lors de la mise à niveau de MS Access> SQL Server.

Un grand nombre de ces tables ont également des index uniques non clusterisés qui sont des doublons de la clé primaire.

J'essaie de nettoyer ça.

Mais ce que j'ai trouvé, c'est après avoir recréé les clés primaires sous forme d'index cluster, puis essayer de reconstruire la clé étrangère, la clé étrangère fait référence à l'ancien index en double (qui était unique).

Je le sais car cela ne me permettra pas de supprimer les index en double.

Je pense que SQL Server choisirait toujours une clé primaire si elle existait. SQL Server a-t-il une méthode pour choisir entre un index unique et une clé primaire?

Pour dupliquer le problème (sur SQL Server 2008 R2):

IF EXISTS (SELECT * FROM sys.tables WHERE name = 'Child') DROP TABLE Child
GO
IF EXISTS (SELECT * FROM sys.tables WHERE name = 'Parent') DROP TABLE Parent
GO

-- Create the parent table
CREATE TABLE Parent (ParentID INT NOT NULL IDENTITY(1,1)) 

-- Make the parent table a heap
ALTER TABLE Parent ADD CONSTRAINT PK_Parent PRIMARY KEY NONCLUSTERED (ParentID) 

-- Create the duplicate index on the parent table
CREATE UNIQUE NONCLUSTERED INDEX IX_Parent ON Parent (ParentID) 

-- Create the child table
CREATE TABLE Child  (ChildID  INT NOT NULL IDENTITY(1,1), ParentID INT NOT NULL ) 

-- Give the child table a normal PKey
ALTER TABLE Child ADD CONSTRAINT PK_Child PRIMARY KEY CLUSTERED (ChildID) 

-- Create a foreign key relationship with the Parent table on ParentID
ALTER TABLE Child ADD CONSTRAINT FK_Child FOREIGN KEY (ParentID) 
REFERENCES Parent (ParentID) ON DELETE CASCADE NOT FOR REPLICATION

-- Try to clean this up
-- Drop the foreign key constraint on the Child table
ALTER TABLE Child DROP CONSTRAINT FK_Child

-- Drop the primary key constraint on the Parent table
ALTER TABLE Parent DROP CONSTRAINT PK_Parent

-- Recreate the primary key on Parent as a clustered index
ALTER TABLE Parent ADD CONSTRAINT PK_Parent PRIMARY KEY CLUSTERED (ParentID) 

-- Recreate the foreign key in Child pointing to parent ID
ALTER TABLE Child ADD CONSTRAINT FK_Child FOREIGN KEY (ParentID) 
REFERENCES Parent (ParentID) ON DELETE CASCADE NOT FOR REPLICATION

-- Try to drop the duplicate index on Parent 
DROP INDEX IX_Parent ON Parent 

Message d'erreur:

Msg 3723, niveau 16, état 6, ligne 36 Un DROP INDEX explicite n'est pas autorisé sur l'index 'Parent.IX_Parent'. Il est utilisé pour l'application des contraintes FOREIGN KEY.

8kb
la source
Les commentaires ne sont pas pour une discussion approfondie; cette conversation a été déplacée vers le chat .
Paul White 9

Réponses:

7

Le (manque de) documentation suggère que ce comportement est un détail d'implémentation et n'est donc pas défini et sujet à changement à tout moment.

C'est en contraste frappant avec CREATE FULLTEXT INDEX , où vous devez spécifier le nom d'un index à attacher - AFAIK, il n'y a pas de FOREIGN KEYsyntaxe non documentée pour faire l'équivalent (bien qu'en théorie, il pourrait y en avoir dans le futur).

Comme mentionné, il est logique que SQL Server choisisse le plus petit index physique auquel associer la clé étrangère. Si vous modifiez le script pour créer la contrainte unique en tant que CLUSTERED, le script "fonctionne" sur 2008 R2. Mais ce comportement n'est pas encore défini et ne doit pas être invoqué.

Comme pour la plupart des applications héritées, il vous suffira de passer aux choses sérieuses et de nettoyer les choses.

Jon Seigel
la source
"SQL Server choisit le plus petit index physique auquel associer la clé étrangère" pas nécessairement en fait. Il y a un exemple dans la réponse voisine où SqlServer choisit un index qui n'est pas de la plus petite taille physique.
i-one
3

SQL Server a-t-il une méthode pour choisir entre un index unique et une clé primaire?

Au moins, il est possible de diriger SqlServer pour référencer la clé primaire, lorsque la clé étrangère est créée et que des contraintes de clé alternatives ou des index uniques existent sur la table référencée.

Si la clé primaire doit être référencée, seul le nom de la table référencée doit être spécifié dans la définition de la clé étrangère et la liste des colonnes référencées doit être omise:

ALTER TABLE Child
    ADD CONSTRAINT FK_Child_Parent FOREIGN KEY (ParentID)
        -- omit key columns of the referenced table
        REFERENCES Parent /*(ParentID)*/;

Plus de détails ci-dessous.


Considérez la configuration suivante:

CREATE TABLE T (id int NOT NULL, a int, b int, c uniqueidentifier, filler binary(1000));
CREATE TABLE TRef (tid int NULL);

où table TRefentend référencer la table T.

Pour créer une contrainte référentielle, on peut utiliser la ALTER TABLEcommande avec deux alternatives:

ALTER TABLE TRef
    ADD CONSTRAINT FK_TRef_T_1 FOREIGN KEY (tid) REFERENCES T (id);

ALTER TABLE TRef
    ADD CONSTRAINT FK_TRef_T_2 FOREIGN KEY (tid) REFERENCES T;

notez que dans le second cas, aucune colonne de la table référencée n'est spécifiée ( REFERENCES Tpar rapport à REFERENCES T (id)).

Puisqu'il n'y a pas Tencore d' index clés sur , l'exécution de ces commandes générera des erreurs.

La première commande renvoie l'erreur suivante:

Msg 1776, niveau 16, état 0, ligne 4

Il n'y a pas de clé primaire ou candidate dans la table référencée 'T' qui correspond à la liste des colonnes de référence dans la clé étrangère 'FK_TRef_T_1'.

La deuxième commande, cependant, renvoie une erreur différente:

Msg 1773, niveau 16, état 0, ligne 4

La clé étrangère 'FK_TRef_T_2' a une référence implicite à l'objet 'T' qui n'a pas de clé primaire définie dessus.

voir que dans le premier cas, l'attente est la clé primaire ou candidate , tandis que dans le second cas, l'attente est la clé primaire uniquement.

Vérifions si SqlServer utilisera autre chose que la clé primaire avec la deuxième commande ou non.

Si nous ajoutons des index uniques et une clé unique sur T:

CREATE UNIQUE INDEX IX_T_1 on T(id) INCLUDE (filler);
CREATE UNIQUE INDEX IX_T_2 on T(id) INCLUDE (c);
CREATE UNIQUE INDEX IX_T_3 ON T(id) INCLUDE (a, b);

ALTER TABLE T
    ADD CONSTRAINT UQ_T UNIQUE CLUSTERED (id);

commande de FK_TRef_T_1création réussit, mais la commande de FK_TRef_T_2création échoue toujours avec Msg 1773.

Enfin, si nous ajoutons la clé primaire sur T:

ALTER TABLE T
    ADD CONSTRAINT PK_T PRIMARY KEY NONCLUSTERED (id);

commande de FK_TRef_T_2création réussit.

Vérifions quels index de la table Tsont référencés par les clés étrangères de la table TRef:

select
    ix.index_id,
    ix.name as index_name,
    ix.type_desc as index_type_desc,
    fk.name as fk_name
from sys.indexes ix
    left join sys.foreign_keys fk on
        fk.referenced_object_id = ix.object_id
        and fk.key_index_id = ix.index_id
        and fk.parent_object_id = object_id('TRef')
where ix.object_id = object_id('T');

cela renvoie:

index_id  index_name  index_type_desc   fk_name
--------- ----------- ----------------- ------------
1         UQ_T        CLUSTERED         NULL
2         IX_T_1      NONCLUSTERED      FK_TRef_T_1
3         IX_T_2      NONCLUSTERED      NULL
4         IX_T_3      NONCLUSTERED      NULL
5         PK_T        NONCLUSTERED      FK_TRef_T_2

voir qui FK_TRef_T_2correspondent à PK_T.

Donc, oui, avec l'utilisation de la REFERENCES Tsyntaxe, la clé étrangère de TRefest mappée à la clé primaire de T.

Je n'ai pas pu trouver un tel comportement décrit directement dans la documentation SqlServer, mais le Msg dédié 1773 suggère que ce n'est pas accidentel. Une telle implémentation est probablement conforme à la norme SQL, voici un court extrait de la section 11.8 de la norme ANSI / ISO 9075-2: 2003

11 Définition et manipulation de schéma

11.8 <définition de la contrainte référentielle>

Fonction
Spécifiez une contrainte référentielle.

Format

<referential constraint definition> ::=
    FOREIGN KEY <left paren> <referencing columns> <right paren>
        <references specification>

<references specification> ::=
    REFERENCES <referenced table and columns>
    [ MATCH <match type> ]
    [ <referential triggered action> ]
...

Règles de syntaxe
...
3) Cas:
...
b) Si la <table et colonnes référencées> ne spécifie pas une <liste de colonnes de référence>, le descripteur de table de la table référencée doit inclure une contrainte unique qui spécifie PRIMARY KEY. Laissez les colonnes référencées être la ou les colonnes identifiées par les colonnes uniques dans cette contrainte unique et laissez la colonne référencée être une de ces colonnes. La <table et colonnes référencées> doit être considérée comme spécifiant implicitement une <liste de colonnes de référence> qui est identique à cette <liste de colonnes unique>.
...

Transact-SQL prend en charge et étend ANSI SQL. Cependant, il n'est pas exactement conforme à la norme SQL. Il existe un document nommé SQL Server Transact-SQL ISO / IEC 9075-2 Standards Support Document (MS-TSQLISO02 en bref, voir ici ) décrivant le niveau de prise en charge fourni par Transact-SQL. Le document répertorie les extensions et les variations de la norme. Par exemple, il documente que la MATCHclause n'est pas prise en charge dans la définition de contrainte référentielle. Mais il n'y a pas de variations documentées pertinentes pour l'élément de norme cité. Donc, mon opinion est que le comportement observé est suffisamment documenté.

Et avec l'utilisation de la REFERENCES T (<reference column list>)syntaxe, il semble que SqlServer sélectionne le premier index non cluster approprié parmi les index de la table référencée (celui avec le moins index_idapparemment, pas celui avec la plus petite taille physique comme supposé dans les commentaires de la question), ou un index cluster si c'est le cas convient et il n'y a pas d'index non cluster adaptés. Un tel comportement semble cohérent depuis SqlServer 2008 (version 10.0). Ceci est juste une observation bien sûr, aucune garantie dans ce cas.

i-one
la source