Indice de unicidad sobrecarga

14

He tenido un debate en curso con varios desarrolladores en mi oficina sobre el costo de un índice y si la unicidad es beneficiosa o costosa (probablemente ambas). El quid de la cuestión son nuestros recursos competidores.

Antecedentes

Anteriormente leí una discusión que decía que un Uniqueíndice no tiene costo adicional para mantener, ya que una Insertoperación verifica implícitamente dónde encaja en el árbol B y, si se encuentra un duplicado en un índice no único, agrega un uniquifier a el final de la clave, pero por lo demás se inserta directamente. En esta secuencia de eventos, un Uniqueíndice no tiene costo adicional.

Mi compañero de trabajo combate esta afirmación diciendo que Uniquese aplica como una segunda operación después de buscar la nueva posición en el árbol B y, por lo tanto, es más costoso de mantener que un índice no único.

En el peor de los casos, he visto tablas con una columna de identidad (inherentemente única) que es la clave de agrupación de la tabla, pero explícitamente declarada como no única. En el otro lado de lo peor está mi obsesión con la unicidad, y todos los índices se crean como únicos, y cuando no es posible definir una relación explícitamente única con un índice, agrego el PK de la tabla al final del índice para asegurar que La unicidad está garantizada.

Con frecuencia participo en revisiones de código para el equipo de desarrollo, y necesito poder dar pautas generales para que sigan. Sí, cada índice debe evaluarse, pero cuando tiene cinco servidores con miles de tablas cada uno y hasta veinte índices en una tabla, debe poder aplicar algunas reglas simples para garantizar un cierto nivel de calidad.

Pregunta

¿La unicidad tiene un costo adicional en el back-end de una Insertcomparación con el costo de mantener un índice no único? En segundo lugar, ¿qué tiene de malo agregar la clave primaria de una tabla al final de un índice para garantizar la unicidad?

Definición de tabla de ejemplo

create table #test_index
    (
    id int not null identity(1, 1),
    dt datetime not null default(current_timestamp),
    val varchar(100) not null,
    is_deleted bit not null default(0),
    primary key nonclustered(id desc),
    unique clustered(dt desc, id desc)
    );

create index
    [nonunique_nonclustered_example]
on #test_index
    (is_deleted)
include
    (val);

create unique index
    [unique_nonclustered_example]
on #test_index
    (is_deleted, dt desc, id desc)
include
    (val);

Ejemplo

Un ejemplo de por qué agregaría la Uniqueclave al final de un índice está en una de nuestras tablas de hechos. Hay una Primary Keyque es una Identitycolumna. Sin embargo, Clustered Indexes la columna del esquema de partición, seguida de tres dimensiones de clave externa sin unicidad. Seleccionar el rendimiento en esta tabla es abismal, y frecuencia me buscan mejores tiempos con el Primary Keycon una búsqueda de claves en lugar de aprovechar el Clustered Index. Otras tablas que siguen un diseño similar, pero que se han Primary Keyagregado al final tienen un rendimiento considerablemente mejor.

-- date_int is equivalent to convert(int, convert(varchar, current_timestamp, 112))
if not exists(select * from sys.partition_functions where [name] = N'pf_date_int')
    create partition function 
        pf_date_int (int) 
    as range right for values 
        (19000101, 20180101, 20180401, 20180701, 20181001, 20190101, 20190401, 20190701);
go

if not exists(select * from sys.partition_schemes where [name] = N'ps_date_int')
    create partition scheme 
        ps_date_int
    as partition 
        pf_date_int all 
    to 
        ([PRIMARY]);
go

if not exists(select * from sys.objects where [object_id] = OBJECT_ID(N'dbo.bad_fact_table'))
    create table dbo.bad_fact_table
        (
        id int not null, -- Identity implemented elsewhere, and CDC populates
        date_int int not null,
        dt date not null,
        group_id int not null,
        group_entity_id int not null, -- member of group
        fk_id int not null,
        -- tons of other columns
        primary key nonclustered(id, date_int),
        index [ci_bad_fact_table] clustered (date_int, group_id, group_entity_id, fk_id)
        )
    on ps_date_int(date_int);
go

if not exists(select * from sys.objects where [object_id] = OBJECT_ID(N'dbo.better_fact_table'))
    create table dbo.better_fact_table
        (
        id int not null, -- Identity implemented elsewhere, and CDC populates
        date_int int not null,
        dt date not null,
        group_id int not null,
        group_entity_id int not null, -- member of group
        -- tons of other columns
        primary key nonclustered(id, date_int),
        index [ci_better_fact_table] clustered(date_int, group_id, group_entity_id, id)
        )
    on ps_date_int(date_int);
go
Solonotix
fuente

Respuestas:

16

Con frecuencia participo en revisiones de código para el equipo de desarrollo, y necesito poder dar pautas generales para que sigan.

El entorno en el que estoy involucrado actualmente tiene 250 servidores con 2500 bases de datos. He trabajado en sistemas con 30,000 bases de datos . Las pautas para la indexación deben girar en torno a la convención de nomenclatura, etc., no deben ser "reglas" para qué columnas incluir en un índice; cada índice individual debe diseñarse para que sea el índice correcto para esa regla o código comercial específico que toque la tabla.

¿La unicidad tiene un costo adicional en el back-end de una Insertcomparación con el costo de mantener un índice no único? En segundo lugar, ¿qué tiene de malo agregar la clave primaria de una tabla al final de un índice para garantizar la unicidad?

Agregar la columna de clave principal al final de un índice no único para que sea único me parece un antipatrón. Si las reglas comerciales dictan que los datos deben ser únicos, agregue una restricción única a la columna; que creará automáticamente un índice único. Si está indexando una columna por rendimiento , ¿por qué agregaría una columna al índice?

Incluso si su suposición de que imponer la unicidad no agrega ninguna sobrecarga adicional es correcta (lo que no es cierto para ciertos casos), ¿qué está resolviendo complicando innecesariamente el índice?

En el caso específico de agregar la clave primaria al final de su clave de índice para que pueda hacer que la definición del índice incluya el UNIQUEmodificador, en realidad no hace ninguna diferencia en la estructura del índice físico en el disco. Esto se debe a la naturaleza de la estructura de las claves de índices del árbol B, ya que siempre deben ser únicas.

Como David Browne mencionó en un comentario:

Como cada índice no agrupado se almacena como índice único, no hay ningún costo adicional al insertarlo en un índice único. De hecho, el único costo adicional sería no declarar una clave candidata como índice único, lo que provocaría que las claves de índice agrupadas se agregaran a las claves de índice.

Tome el siguiente ejemplo mínimamente completo y verificable :

USE tempdb;

DROP TABLE IF EXISTS dbo.IndexTest;
CREATE TABLE dbo.IndexTest
(
    id int NOT NULL
        CONSTRAINT IndexTest_pk
        PRIMARY KEY
        CLUSTERED
        IDENTITY(1,1)
    , rowDate datetime NOT NULL
);

Agregaré dos índices que son idénticos, excepto por la adición de la clave primaria al final de la definición de la segunda clave de índices:

CREATE INDEX IndexTest_rowDate_ix01
ON dbo.IndexTest(rowDate);

CREATE UNIQUE INDEX IndexTest_rowDate_ix02
ON dbo.IndexTest(rowDate, id);

A continuación, vamos a varias filas a la tabla:

INSERT INTO dbo.IndexTest (rowDate)
VALUES (DATEADD(SECOND, 0, GETDATE()))
     , (DATEADD(SECOND, 0, GETDATE()))
     , (DATEADD(SECOND, 0, GETDATE()))
     , (DATEADD(SECOND, 1, GETDATE()))
     , (DATEADD(SECOND, 2, GETDATE()));

Como puede ver arriba, tres filas contienen el mismo valor para la rowDatecolumna y dos filas contienen valores únicos.

A continuación, veremos las estructuras de página físicas para cada índice, utilizando el DBCC PAGEcomando no documentado :

DECLARE @dbid int = DB_ID();
DECLARE @fileid int;
DECLARE @pageid int;
DECLARE @indexid int;

SELECT @fileid = ddpa.allocated_page_file_id
    , @pageid = ddpa.allocated_page_page_id
FROM sys.indexes i 
CROSS APPLY sys.dm_db_database_page_allocations(DB_ID(), i.object_id, i.index_id, NULL, 'LIMITED') ddpa
WHERE i.name = N'IndexTest_rowDate_ix01'
    AND ddpa.is_allocated = 1
    AND ddpa.is_iam_page = 0;

PRINT N'*************************************** IndexTest_rowDate_ix01 *****************************************';
DBCC TRACEON(3604);
DBCC PAGE (@dbid, @fileid, @pageid, 1);
DBCC TRACEON(3604);
PRINT N'*************************************** IndexTest_rowDate_ix01 *****************************************';

SELECT @fileid = ddpa.allocated_page_file_id
    , @pageid = ddpa.allocated_page_page_id
FROM sys.indexes i 
CROSS APPLY sys.dm_db_database_page_allocations(DB_ID(), i.object_id, i.index_id, NULL, 'LIMITED') ddpa
WHERE i.name = N'IndexTest_rowDate_ix02'
    AND ddpa.is_allocated = 1
    AND ddpa.is_iam_page = 0;

PRINT N'*************************************** IndexTest_rowDate_ix02 *****************************************';
DBCC TRACEON(3604);
DBCC PAGE (@dbid, @fileid, @pageid, 1);
DBCC TRACEON(3604);
PRINT N'*************************************** IndexTest_rowDate_ix02 *****************************************';

Observé el resultado con Beyond Compare y, a excepción de las diferencias obvias en torno a las ID de las páginas de asignación, etc., las dos estructuras de índice son idénticas.

ingrese la descripción de la imagen aquí

Puede tomar lo anterior para significar que incluir la clave principal en cada índice y definirlo como único es A Good Thing ™, ya que eso es lo que sucede de forma encubierta de todos modos. No haría esa suposición, y sugeriría solo definir un índice como único si, de hecho, los datos naturales en el índice ya son únicos.

Hay varios recursos excelentes en Interwebz sobre este tema, que incluyen:

Para su información, la mera presencia de una identitycolumna no garantiza la unicidad. Debe definir la columna como una clave principal o con una restricción única para garantizar que los valores almacenados en esa columna sean de hecho únicos. La SET IDENTITY_INSERT schema.table ON;declaración le permitirá insertar valores no únicos en una columna definida como identity.

Max Vernon
fuente
5

Solo un complemento de la excelente respuesta de Max .

Cuando se trata de crear un índice agrupado no único, SQL Server crea algo llamado Uniquifieren segundo plano de todos modos.

Esto Uniquifierpodría causar problemas potenciales en el futuro si su plataforma tiene muchas operaciones CRUD, ya Uniquifierque solo tiene 4 bytes (un número entero básico de 32 bits). Por lo tanto, si su sistema tiene muchas operaciones CRUD, es posible que use todos los números únicos disponibles y de repente reciba un error y no le permitirá insertar más datos en sus tablas (porque lo hará ya no tiene valores únicos para asignar a sus filas recién insertadas).

Cuando esto suceda, recibirá este error:

The maximum system-generated unique value for a duplicate group 
was exceeded for index with partition ID (someID). 

Dropping and re-creating the index may resolve this;
otherwise, use another clustering key.

El error 666 (el error anterior) se produce cuando uniquifierun conjunto único de claves no únicas consume más de 2.147.483.647 filas.

Por lo tanto, necesitará tener ~ 2 mil millones de filas para un solo valor clave, o deberá haber modificado un solo valor clave ~ 2 mil millones de veces para ver este error. Como tal, no es extremadamente probable que te encuentres con esta limitación.

Chessbrain
fuente
No tenía idea de que el uniquifier oculto podría quedarse sin espacio clave, pero supongo que todas las cosas son limitadas en algunos casos. Al igual que las estructuras Casey cómo Ifse limitan a 10 niveles, tiene sentido que también haya un límite para resolver entidades no únicas. Según su declaración, parece que solo se aplica a casos en los que la clave de agrupación no es única. ¿Es esto un problema para Nonclustered Indexo si la clave de agrupación es Uniqueentonces no hay un problema para los Nonclusteredíndices?
Solonotix
Un índice único está (hasta donde yo sé) limitado por el tamaño del tipo de columna (por lo tanto, si es un tipo BIGINT, tiene 8bytes para trabajar). Además, según la documentación oficial de Microsoft, hay un máximo de 900bytes permitidos para un índice agrupado y 1700bytes para no agrupado (ya que puede tener más de un índice no agrupado y solo 1 índice agrupado por tabla). docs.microsoft.com/en-us/sql/sql-server/…
Chessbrain
1
@Solonotix: el uniquifier del índice agrupado se usa en los índices no agrupados. Si ejecuta el código en mi ejemplo sin la clave primaria (cree un índice agrupado en su lugar), puede ver que la salida es la misma para los índices únicos y no únicos.
Max Vernon
-2

No voy a analizar la cuestión de si un índice debe ser único o no, y si hay más gastos generales en este enfoque o en ese. Pero un par de cosas me molestaron en tu diseño general

  1. dt datetime no es nulo predeterminado (current_timestamp). Datetime es una forma más antigua o esta, y es posible que pueda lograr al menos algunos ahorros de espacio utilizando datetime2 () y sysdatetime ().
  2. crear índice [nonunique_nonclustered_example] en #test_index (is_deleted) include (val). Esto me molesta Eche un vistazo a cómo se debe acceder a los datos (apuesto a que hay más de WHERE is_deleted = 0) y mire usando un índice filtrado. Incluso consideraría usar 2 índices filtrados, uno para where is_deleted = 0y el otro parawhere is_deleted = 1

Básicamente, esto se parece más a un ejercicio de codificación diseñado para probar una hipótesis que a un problema / solución real, pero esos dos patrones son definitivamente algo que busco en las revisiones de código.

Toby
fuente
Lo máximo que ahorrará usando datetime2 en lugar de datetime es 1 byte, y eso es si su precisión es inferior a 3, lo que significaría perder precisión en segundos fraccionarios, lo que no siempre es una solución viable. En cuanto al índice de ejemplo proporcionado, el diseño se mantuvo simple para centrarse en mi pregunta. Un Nonclusteredíndice tendrá la clave de agrupación agregada al final de la fila de datos para búsquedas de clave internamente. Como tal, los dos índices son físicamente iguales, que fue el punto de mi pregunta.
Solonotix
En la escala que corremos al guardar un byte o dos se suman rápidamente. Y supuse que, dado que usaba la fecha y hora imprecisa, podríamos reducir la precisión. Para los índices, nuevamente declararé que las columnas de bits como las columnas principales en los índices es un patrón que trato como una mala elección. Como con todas las cosas, su kilometraje puede variar. Por desgracia, las desventajas de un modelo aproximado.
Toby el
-4

Parece que simplemente estás usando PK para hacer un índice alternativo más pequeño. Por lo tanto, el rendimiento es más rápido.

Puede ver esto en empresas que tienen tablas de datos masivas (por ejemplo, tablas de datos maestros). Alguien decide tener un índice agrupado masivo esperando que satisfaga las necesidades de varios grupos de informes.

Pero, un grupo puede necesitar solo unas pocas partes de ese índice, mientras que otro grupo necesita otras partes ... por lo que el índice simplemente golpeando en cada columna bajo el sol para "optimizar el rendimiento" realmente no ayuda.

Mientras tanto, desglosarlo para crear índices múltiples, más pequeños y específicos, a menudo resuelve el problema.

Y, eso parece ser lo que estás haciendo. Tiene este índice agrupado masivo con un rendimiento horrible, luego está usando PK para crear otro índice con menos columnas que (no es de extrañar) tiene un mejor rendimiento.

Entonces, solo haga un análisis y descubra si puede tomar el índice agrupado único y dividirlo en índices más pequeños y específicos que necesitan trabajos específicos.

Tendría que analizar el rendimiento desde un punto de vista de "índice único versus índice múltiple", porque hay gastos generales en la creación y actualización de índices. Pero, tienes que analizar esto desde una perspectiva general.

EG: puede ser menos intensivo en recursos para un índice agrupado masivo, y más intensivo en recursos para tener varios índices específicos más pequeños. Pero, si puede ejecutar consultas específicas en el back-end mucho más rápido, ahorrando tiempo (y dinero) allí, podría valer la pena.

Por lo tanto, tendría que hacer un análisis de extremo a extremo ... no solo ver cómo afecta a su propio mundo, sino también cómo afecta a los usuarios finales.

Siento que estás usando mal el identificador PK. Pero, puede estar utilizando un sistema de base de datos que solo permite 1 índice (?), Pero puede introducir otro si su PK (b / c cada sistema de base de datos relacional en estos días parece indexar automáticamente el PK). Sin embargo, la mayoría de los RDBMS modernos deberían permitir la creación de múltiples índices; no debe haber límite para la cantidad de índices que puede hacer (en oposición a un límite de 1 PK).

Entonces, al hacer un PK que solo actúa como un índice alternativo ... estás usando tu PK, que puede ser necesario si la tabla se expande más tarde en su rol.

Eso no quiere decir que su mesa no necesita un PK .. SOP DB's 101 dice "cada mesa debe tener un PK". Pero, en una situación de almacenamiento de datos o similar ... tener una PK en una tabla puede ser una carga adicional que no necesita. O bien, podría ser un envío de Dios para asegurarse de que no está agregando doblemente entradas engañosas. Realmente es una cuestión de lo que estás haciendo y por qué lo estás haciendo.

Pero, las tablas masivas definitivamente se benefician de tener índices. Pero, suponiendo que un solo índice agrupado masivo sea lo mejor es solo ... puede ser el mejor ... pero recomendaría probar en un entorno de prueba dividiendo el índice en múltiples índices más pequeños dirigidos a escenarios de casos de uso específicos.

blahblah
fuente