¿Cómo elige SQL Server una clave de índice para una referencia de clave externa?

9

Estoy trabajando con una base de datos heredada que se importó de MS Access. Hay alrededor de veinte tablas con claves primarias únicas no agrupadas que se crearon durante la actualización de MS Access> SQL Server.

Muchas de estas tablas también tienen índices únicos no agrupados que son duplicados de la clave primaria.

Estoy intentando limpiar esto.

Pero lo que he encontrado es que después de recrear las claves primarias como índices agrupados, y luego intentar reconstruir la clave externa, la clave externa hace referencia al índice antiguo y duplicado (que era único).

Sé esto porque no me deja caer los índices duplicados.

Creo que SQL Server siempre elegiría una clave primaria si existiera. ¿SQL Server tiene un método para elegir entre un índice único y una clave primaria?

Para duplicar el problema (en 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 

Mensaje de error:

Msg 3723, Nivel 16, Estado 6, Línea 36 No se permite un ÍNDICE DE GOTA explícito en el índice 'Parent.IX_Parent'. Se está utilizando para la aplicación de restricciones FOREIGN KEY.

8kb
fuente
Los comentarios no son para discusión extendida; Esta conversación se ha movido al chat .
Paul White 9

Respuestas:

7

La (falta de) documentación sugiere que este comportamiento es un detalle de implementación y, por lo tanto, no está definido y está sujeto a cambios en cualquier momento.

Esto está en marcado contraste con CREATE FULLTEXT INDEX , donde debe especificar el nombre de un índice para adjuntar: AFAIK, no hay FOREIGN KEYsintaxis indocumentada para hacer el equivalente (aunque en teoría, podría existir en el futuro).

Como se mencionó, tiene sentido que SQL Server elija el índice físico más pequeño con el que asociar la clave externa. Si cambia el script para crear la restricción única como CLUSTERED, el script "funciona" en 2008 R2. Pero ese comportamiento aún no está definido y no se debe confiar en él.

Al igual que con la mayoría de las aplicaciones heredadas, solo tendrá que ir al grano y limpiar las cosas.

Jon Seigel
fuente
"SQL Server elige el índice físico más pequeño con el que asociar la clave externa" no necesariamente en realidad. Hay un ejemplo en la respuesta contigua donde SqlServer elige un índice que no es del tamaño físico más pequeño.
i-uno
3

¿SQL Server tiene un método para elegir entre un índice único y una clave primaria?

Al menos es posible dirigir SqlServer para que haga referencia a la clave primaria, cuando se crea una clave externa y existen restricciones de clave alternativas o índices únicos en la tabla a la que se hace referencia.

Si es necesario hacer referencia a la clave primaria, solo se debe especificar el nombre de la tabla a la que se hace referencia en la definición de clave externa y se debe omitir la lista de columnas a las que se hace referencia:

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

Más detalles a continuación.


Considere la siguiente configuración:

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

donde tabla TRefpretende hacer referencia a la tabla T.

Para crear una restricción referencial, se puede usar el ALTER TABLEcomando con dos alternativas:

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;

observe que en el segundo caso no se especifican columnas de la tabla a la que se hace referencia ( REFERENCES Tversus REFERENCES T (id)).

Como todavía no hay índices clave activados T, la ejecución de estos comandos generará errores.

El primer comando devuelve el siguiente error:

Mensaje 1776, Nivel 16, Estado 0, Línea 4

No hay claves principales o candidatas en la tabla referenciada 'T' que coincidan con la lista de columnas de referencia en la clave externa 'FK_TRef_T_1'.

El segundo comando, sin embargo, devuelve un error diferente:

Mensaje 1773, Nivel 16, Estado 0, Línea 4

La clave externa 'FK_TRef_T_2' tiene una referencia implícita al objeto 'T' que no tiene una clave primaria definida.

vea que en el primer caso la expectativa es clave primaria o candidata , mientras que en el segundo caso la expectativa es clave primaria solamente.

Verifiquemos si SqlServer usará algo diferente a la clave primaria con el segundo comando o no.

Si agregamos algunos índices únicos y una clave única en 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);

el comando para la FK_TRef_T_1creación tiene éxito, pero el comando para la FK_TRef_T_2creación todavía falla con Msg 1773.

Finalmente, si agregamos clave primaria en T:

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

comando para la FK_TRef_T_2creación tiene éxito.

Vamos a ver qué índices de la tabla Thacen referencia las claves externas de la tabla 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');

esto devuelve:

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

ver que FK_TRef_T_2corresponde a PK_T.

Entonces, sí, con el uso de la REFERENCES Tsintaxis, la clave externa de TRefse asigna a la clave primaria de T.

No pude encontrar el comportamiento descrito en la documentación de SqlServer directamente, pero Msg 1773 dedicado sugiere que no es accidental. Es probable que dicha implementación cumpla con el Estándar SQL, a continuación se incluye un breve extracto de la sección 11.8 de ANSI / ISO 9075-2: 2003

11 Definición y manipulación de esquemas

11.8 <definición de restricción referencial>

Función
Especifica una restricción referencial.

Formato

<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> ]
...

Reglas de sintaxis
...
3) Caso:
...
b) Si la <tabla y columnas referenciadas> no especifica una <lista de columnas de referencia>, entonces el descriptor de la tabla referenciada incluirá una restricción única que especifica la CLAVE PRIMARIA. Deje que las columnas de referencia sean la columna o columnas identificadas por las columnas únicas en esa restricción única y deje que la columna de referencia sea ​​una de esas columnas. Se considerará que la <tabla y columnas referenciadas> especifica implícitamente una <lista de columnas de referencia> que es idéntica a esa <lista de columnas única>.
...

Transact-SQL admite y extiende ANSI SQL. Sin embargo, no se ajusta exactamente al estándar SQL. Existe un documento denominado Documento de soporte de estándares ISO / IEC 9075-2 de SQL Server Transact-SQL (MS-TSQLISO02 en resumen, consulte aquí ) que describe el nivel de soporte que proporciona Transact-SQL. El documento enumera extensiones y variaciones al estándar. Por ejemplo, documenta que la MATCHcláusula no se admite en la definición de restricción referencial. Pero no hay variaciones documentadas relevantes para la norma mencionada. Entonces, mi opinión es que el comportamiento observado está suficientemente documentado.

Y con el uso de la REFERENCES T (<reference column list>)sintaxis, parece que SqlServer selecciona el primer índice no agrupado adecuado entre los índices de la tabla a la que se hace referencia (el que tiene la menor cantidad index_idaparente, no el que tiene el tamaño físico más pequeño como se supone en los comentarios de la pregunta), o el índice agrupado si es trajes y no hay índices no agrupados adecuados. Tal comportamiento parece ser consistente desde SqlServer 2008 (versión 10.0). Esto es solo observación, por supuesto, no hay garantías en este caso.

i-one
fuente