Exclusividade do índice

14

Eu tenho um debate em andamento com vários desenvolvedores em meu escritório sobre o custo de um índice e se a exclusividade é ou não benéfica ou cara (provavelmente as duas). O cerne da questão são nossos recursos concorrentes.

fundo

Eu li anteriormente uma discussão que afirmava que um Uniqueíndice não tem custo adicional de manutenção, uma vez que uma Insertoperação verifica implicitamente onde ele se encaixa na árvore B e, se uma duplicata for encontrada em um índice não exclusivo, anexa um unificador final da chave, mas, caso contrário, é inserido diretamente. Nesta sequência de eventos, um Uniqueíndice não tem custo adicional.

Meu colega de trabalho combate essa afirmação dizendo que Uniqueé imposta como uma segunda operação após a busca pela nova posição na árvore B e, portanto, é mais cara de manter do que um índice não exclusivo.

Na pior das hipóteses, vi tabelas com uma coluna de identidade (inerentemente exclusiva) que é a chave de cluster da tabela, mas declarada explicitamente como não exclusiva. Do outro lado do pior, está minha obsessão pela exclusividade, e todos os índices são criados como únicos e, quando não é possível definir uma relação explicitamente exclusiva com um índice, anexo a PK da tabela ao final do índice para garantir que o a exclusividade é garantida.

Estou freqüentemente envolvido em revisões de código para a equipe de desenvolvimento e preciso fornecer diretrizes gerais para que eles sigam. Sim, todos os índices devem ser avaliados, mas quando você tem cinco servidores com milhares de tabelas cada e até vinte índices em uma tabela, precisa aplicar algumas regras simples para garantir um certo nível de qualidade.

Questão

A exclusividade tem um custo adicional no final de uma Insertcomparação com o custo de manutenção de um índice não exclusivo? Em segundo lugar, o que há de errado em acrescentar a Chave Primária de uma tabela ao final de um índice para garantir a exclusividade?

Exemplo de definição de tabela

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);

Exemplo

Um exemplo de por que eu adicionaria a Uniquechave ao final de um índice está em uma de nossas tabelas de fatos. Existe um Primary Keyque é uma Identitycoluna. No entanto, Clustered Indexé a coluna do esquema de particionamento, seguida por três dimensões de chave estrangeira sem exclusividade. O desempenho selecionado nesta tabela é péssimo e, frequentemente, os tempos de busca são melhores usando o Primary Keycom uma pesquisa de chave, em vez de alavancar o Clustered Index. Outras tabelas que seguem um design semelhante, mas Primary Keyanexadas ao final, têm desempenho consideravelmente melhor.

-- 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
fonte

Respostas:

16

Estou freqüentemente envolvido em revisões de código para a equipe de desenvolvimento e preciso fornecer diretrizes gerais para que eles sigam.

O ambiente em que estou envolvido atualmente possui 250 servidores com 2500 bancos de dados. Eu trabalhei em sistemas com 30.000 bancos de dados . As diretrizes para indexação devem girar em torno da convenção de nomenclatura, etc., não devem ser "regras" para quais colunas incluir em um índice - todo índice individual deve ser projetado para ser o índice correto para essa regra ou código comercial específico que toca a tabela.

A exclusividade tem um custo adicional no final de uma Insertcomparação com o custo de manutenção de um índice não exclusivo? Em segundo lugar, o que há de errado em acrescentar a Chave Primária de uma tabela ao final de um índice para garantir a exclusividade?

Adicionar a coluna da chave primária ao final de um índice não exclusivo para torná-lo exclusivo parece-me um anti-padrão. Se as regras de negócios determinarem que os dados sejam exclusivos, adicione uma restrição exclusiva à coluna; que criará automaticamente um índice exclusivo. Se você estiver indexando uma coluna para obter desempenho , por que você adicionaria uma coluna ao índice?

Mesmo que sua suposição de que impor a exclusividade não inclua nenhuma sobrecarga extra esteja correta (o que não acontece em certos casos), o que você está resolvendo complicando desnecessariamente o índice?

Na instância específica de adicionar a chave primária ao final da sua chave de índice, para que você possa fazer com que a definição do índice inclua o UNIQUEmodificador, ela realmente faz diferença zero na estrutura do índice físico no disco. Isso se deve à natureza da estrutura das chaves de índices da árvore B, pois elas sempre precisam ser únicas.

Como David Browne mencionou em um comentário:

Como todo índice não clusterizado é armazenado como índice exclusivo, não há custo extra na inserção em um índice exclusivo. De fato, o único custo extra seria não declarar uma chave candidata como um índice exclusivo, o que faria com que as chaves de índice agrupadas fossem anexadas às chaves de índice.

Veja o seguinte exemplo minimamente completo e verificável :

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
);

Vou adicionar dois índices idênticos, exceto pela adição da chave primária no final da segunda definição de chave dos índices:

CREATE INDEX IndexTest_rowDate_ix01
ON dbo.IndexTest(rowDate);

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

Em seguida, apresentaremos várias linhas na tabela:

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 você pode ver acima, três linhas contêm o mesmo valor para a rowDatecoluna e duas linhas contêm valores exclusivos.

A seguir, veremos as estruturas físicas da página para cada índice, usando o DBCC PAGEcomando não 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 *****************************************';

Analisei a saída usando o Beyond Compare e, exceto por diferenças óbvias nos IDs da página de alocação, etc., as duas estruturas de índice são idênticas.

insira a descrição da imagem aqui

Você pode considerar o que foi exposto acima, que inclui a chave primária em todos os índices e definir como único é A Good Thing ™, pois é o que acontece de qualquer maneira. Eu não faria essa suposição e sugeriria apenas definir um índice como único se, de fato, os dados naturais no índice já forem únicos.

Existem vários recursos excelentes na Interwebz sobre esse tópico, incluindo:

Para sua informação, a mera presença de uma identitycoluna não garante exclusividade. Você precisa definir a coluna como uma chave primária ou com uma restrição exclusiva para garantir que os valores armazenados nessa coluna sejam realmente exclusivos. A SET IDENTITY_INSERT schema.table ON;instrução permitirá que você insira valores não exclusivos em uma coluna definida como identity.

Max Vernon
fonte
5

Apenas um complemento à excelente resposta de Max .

Quando se trata de criar um índice em cluster não exclusivo, o SQL Server cria algo chamado a Uniquifierem segundo plano de qualquer maneira.

Isso Uniquifierpode causar problemas em potencial no futuro, se a sua plataforma tiver muitas operações CRUD, uma vez que esse Uniquifiertamanho é de apenas 4 bytes (um número inteiro de 32 bits). Portanto, se seu sistema tiver muitas operações CRUD, é possível que você use todos os números exclusivos disponíveis e, de repente, você receberá um erro e isso não permitirá que você insira mais dados em suas tabelas (porque não possui mais valores exclusivos para atribuir às linhas recém-inseridas).

Quando isso acontecer, você receberá este erro:

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.

O erro 666 (o erro acima) ocorre quando o uniquifierpara um único conjunto de chaves não exclusivas consome mais de 2.147.483.647 linhas.

Portanto, você precisará ter ~ 2 bilhões de linhas para um único valor de chave ou precisará modificar um valor único de chave ~ 2 bilhões de vezes para ver esse erro. Como tal, não é extremamente provável que você encontre essa limitação.

Chessbrain
fonte
Eu não tinha ideia de que o uniquificador oculto poderia ficar sem espaço chave, mas acho que todas as coisas são limitadas em alguns casos. Assim como as estruturas Casee os Iflimites são limitados a 10 níveis, faz sentido que também haja um limite para a resolução de entidades não exclusivas. Pela sua declaração, isso parece aplicar-se apenas a casos em que a chave de cluster não é exclusiva. Isso é um problema para um Nonclustered Indexou se a chave de cluster é Uniqueentão não há um problema para Nonclusteredíndices?
Solonotix 15/07
Um índice exclusivo é (até onde eu sei) limitado pelo tamanho do tipo de coluna (portanto, se for do tipo BIGINT, você terá 8 bytes para trabalhar). Além disso, de acordo com a documentação oficial da Microsoft, há um máximo de 900 bytes permitido para um índice clusterizado e 1700 bytes para não clusterizado (já que você pode ter mais de um índice não clusterizado e apenas 1 índice clusterizado por tabela). docs.microsoft.com/en-us/sql/sql-server/…
Chessbrain
1
@Solonotix - o uniquificador do índice clusterizado é usado nos índices não clusterizados. Se você executar o código no meu exemplo sem a chave primária (crie um índice em cluster), poderá ver que a saída é a mesma para os índices não exclusivo e exclusivo.
Max Vernon
-2

Não vou me aprofundar na questão de saber se um índice deve ser único ou não, e se há mais sobrecarga nessa abordagem ou naquilo. Mas algumas coisas me incomodaram no seu design geral

  1. dt datetime não nulo padrão (current_timestamp). Datetime é uma forma mais antiga, e você pode conseguir pelo menos algumas economias de espaço usando datetime2 () e sysdatetime ().
  2. crie um índice [nonunique_nonclustered_example] em #test_index (is_deleted) include (val). Isso me incomoda. Dê uma olhada em como os dados devem ser acessados ​​(aposto que há mais do que WHERE is_deleted = 0) e veja usando um índice filtrado. Eu consideraria até usar dois índices filtrados, um para where is_deleted = 0e outro parawhere is_deleted = 1

Fundamentalmente, isso parece mais um exercício de codificação projetado para testar uma hipótese do que um problema / solução real, mas esses dois padrões são definitivamente algo que procuro nas revisões de código.

Toby
fonte
O máximo que você economizará usando datetime2 em vez de datetime é de 1 byte, ou seja, se sua precisão for menor que 3, o que significaria perder precisão em segundos fracionários, o que nem sempre é uma solução viável. Quanto ao exemplo de índice fornecido, o design foi mantido simples para se concentrar na minha pergunta. Um Nonclusteredíndice terá a chave de cluster anexada ao final da linha de dados para consultas de chave internamente. Como tal, os dois índices são fisicamente os mesmos, que foi o ponto da minha pergunta.
Solonotix 23/07/19
Na escala, tentamos salvar um byte ou dois rapidamente. E eu assumi que, desde que você usasse o horário impreciso, poderíamos reduzir a precisão. Para os índices, novamente declararei que as colunas de bits como as colunas principais nos índices são um padrão que trato como uma má escolha. Como em todas as coisas, sua milhagem pode variar. Infelizmente, as desvantagens de um modelo aproximado.
Toby
-4

Parece que você está simplesmente usando o PK para criar um índice menor e alternativo. Portanto, o desempenho é mais rápido.

Você vê isso em empresas que possuem grandes tabelas de dados (por exemplo: tabelas de dados mestre). Alguém decide ter um índice agrupado massivo, esperando que ele preencha as necessidades de vários grupos de relatórios.

Porém, um grupo pode precisar de apenas algumas partes desse índice, enquanto outro grupo precisa de outras partes. Portanto, o índice apenas batendo em todas as colunas sob o sol para "otimizar o desempenho" não ajuda muito.

Enquanto isso, decompô-lo para criar vários índices menores e direcionados, geralmente resolve o problema.

E isso parece ser o que você está fazendo. Você tem esse índice em cluster maciço com desempenho terrível e, em seguida, usa o PK para criar outro índice com menos colunas que (sem surpresa) tenham melhor desempenho.

Portanto, basta fazer uma análise e descobrir se você pode pegar o único índice agrupado e dividi-lo em índices menores e direcionados, necessários para tarefas específicas.

Você precisaria analisar o desempenho do ponto de vista do "índice único versus índice múltiplo", porque há uma sobrecarga na criação e atualização de índices. Mas você precisa analisar isso de uma perspectiva geral.

EG: pode ser menos intensivo em recursos para um índice clusterizado massivo e mais intensivo em recursos para ter vários índices direcionados menores. Porém, se você conseguir executar consultas direcionadas no back-end com muito mais rapidez, economizando tempo (e dinheiro) lá, pode valer a pena.

Portanto, você teria que fazer uma análise de ponta a ponta ... não apenas ver como isso afeta seu próprio mundo, mas também como isso afeta os usuários finais.

Eu apenas sinto que você está usando mal o identificador PK. Porém, você pode estar usando um sistema de banco de dados que permite apenas 1 índice (?), Mas pode entrar com outro código se fizer PK (b / c todos os sistemas de banco de dados relacional atualmente parece indexar automaticamente o PK). No entanto, a maioria dos RDBMS modernos deve permitir a criação de vários índices; não deve haver limite para o número de índices que você pode criar (em oposição a um limite de 1 PK).

Portanto, ao criar um PK whicih, apenas atua como um índice alt. Você está usando seu PK, o que pode ser necessário se a tabela for expandida posteriormente em sua função.

Isso não quer dizer que sua mesa não precise de um PK. O SOP DB's 101 diz "toda mesa deve ter um PK". Mas, em uma situação de armazenamento de dados ou algo assim, ter uma PK em uma tabela pode ser apenas uma sobrecarga extra que você não precisa. Ou pode ser um envio divino para garantir que você não esteja adicionando duplamente entradas falsas. É realmente uma questão do que você está fazendo e por que está fazendo.

Mas, tabelas maciças definitivamente se beneficiam de ter índices. Mas, supondo que um único índice clusterizado maciço seja o melhor é apenas ... pode ser o melhor .. mas eu recomendo testar em um ambiente de teste dividindo o índice em vários índices menores, visando cenários de casos de uso específicos.

blahblah
fonte