Database
 sql >> Base de Dados >  >> RDS >> Database

Um caso de uso simples para índices em chaves primárias

Introdução


Recentemente, encontramos um problema de desempenho interessante em um de nossos bancos de dados SQL Server que processam transações a uma taxa séria. A tabela de transações usada para capturar essas transações tornou-se uma tabela quente. Como resultado, o problema apareceu na camada de aplicação. Era um tempo limite intermitente da sessão que procurava postar transações.

Isso acontecia porque uma sessão normalmente “segurava” a tabela e causava uma série de bloqueios espúrios no banco de dados.

A primeira reação de um administrador de banco de dados típico seria identificar a sessão de bloqueio primária e encerrá-la com segurança. Isso era seguro porque normalmente era uma instrução SELECT ou uma sessão ociosa.

Houve também outras tentativas de resolver o problema:
  • Limpando a mesa. Esperava-se que isso garantisse um bom desempenho, mesmo que a consulta tivesse que varrer uma tabela inteira.
  • Ativar o nível de isolamento READ COMMITTED SNAPSHOT para reduzir o impacto das sessões de bloqueio.

Neste artigo, tentaremos recriar uma versão simplista do cenário e usá-la para mostrar como a indexação simples pode resolver situações como essa quando bem feita.

Duas Tabelas Relacionadas


Dê uma olhada na Listagem 1 e na Listagem 2. Elas mostram as versões simplificadas das tabelas envolvidas no cenário em consideração.
-- Listing 1: Create TranLog Table

use DB2
go
create table TranLog (
TranID INT IDENTITY(1,1)
,CustomerID char(4)
,ProductCount INT
,TotalPrice Money
,TranTime Timestamp
)
-- Listing 2: Create TranDetails Table

use DB2
go
create table TranDetails (
TranDetailsID INT IDENTITY(1,1)
,TranID INT
,ProductCode uniqueidentifier
,UnitCost Money
,ProductCount INT
,TotalPrice Money
)

A Listagem 3 mostra um gatilho que insere quatro linhas em TranDetails tabela para cada linha inserida no TranLog tabela.
-- Listing 3: Create Trigger

CREATE TRIGGER dbo.GenerateDetails
   ON  dbo.TranLog
   AFTER INSERT
AS 
BEGIN
	-- SET NOCOUNT ON added to prevent extra result sets from
	-- interfering with SELECT statements.
	SET NOCOUNT ON;

insert into dbo.TranDetails (TranID, ProductCode,UnitCost, ProductCount, TotalPrice)
select top 1 dbo.TranLog.TranID, NEWID(), dbo.TranLog.TotalPrice/dbo.TranLog.ProductCount, dbo.TranLog.ProductCount, dbo.TranLog.TotalPrice
from dbo.TranLog order by TranID desc;

insert into dbo.TranDetails (TranID, ProductCode,UnitCost, ProductCount, TotalPrice)
select top 1 dbo.TranLog.TranID, NEWID(), dbo.TranLog.TotalPrice/dbo.TranLog.ProductCount, dbo.TranLog.ProductCount, dbo.TranLog.TotalPrice
from dbo.TranLog order by TranID desc;

insert into dbo.TranDetails (TranID, ProductCode,UnitCost, ProductCount, TotalPrice)
select top 1 dbo.TranLog.TranID, NEWID(), dbo.TranLog.TotalPrice/dbo.TranLog.ProductCount, dbo.TranLog.ProductCount, dbo.TranLog.TotalPrice
from dbo.TranLog order by TranID desc;

insert into dbo.TranDetails (TranID, ProductCode,UnitCost, ProductCount, TotalPrice)
select top 1 dbo.TranLog.TranID, NEWID(), dbo.TranLog.TotalPrice/dbo.TranLog.ProductCount, dbo.TranLog.ProductCount, dbo.TranLog.TotalPrice
from dbo.TranLog order by TranID desc;

END
GO

Participar da consulta


É comum encontrar tabelas de transações suportadas por tabelas grandes. O objetivo é manter transações muito mais antigas ou armazenar os detalhes dos registros resumidos na primeira tabela. Pense nisso como pedidos e detalhes do pedido tabelas típicas em bancos de dados de exemplo do SQL Server. No nosso caso, estamos considerando o TranLog e TranDetails mesas.

Em circunstâncias normais, as transações preenchem essas duas tabelas ao longo do tempo. Em termos de relatórios ou consultas simples, a consulta realizará uma junção nessas duas tabelas. Essa junção capitalizará uma coluna comum entre as tabelas.

Primeiro, preenchemos a tabela usando a consulta na Listagem 4.
-- Listing 4: Insert Rows in TranLog
use DB2
go
insert into TranLog values ('CU01', 5, '50.45', DEFAULT);
insert into TranLog values ('CU02', 7, '42.35', DEFAULT);
insert into TranLog values ('CU03', 15, '39.55', DEFAULT);
insert into TranLog values ('CU04', 9, '33.50', DEFAULT);
insert into TranLog values ('CU05', 2, '105.45', DEFAULT);
go 1000

use DB2
go
select * from TranLog;
select * from TranDetails;

Em nosso exemplo, a coluna comum usada pela junção é o TranID coluna:
-- Listing 5 Join Query
-- 5a
select * from TranLog a join TranDetails b
on a.TranID=b.TranID where a.CustomerID='CU03';

-- 5b
select * from TranLog a join TranDetails b
on a.TranID=b.TranID where a.TranID=30;

Você pode ver as duas consultas de amostra simples que usam uma junção para recuperar registros de TranLog e TranDetails .

Quando executamos as consultas na Listagem 5, em ambos os casos, temos que fazer uma varredura completa em ambas as tabelas (veja as Figuras 1 e 2). A parte dominante de cada consulta são as operações físicas. Ambos são junções internas. No entanto, a Listagem 5a usa uma Correspondência de hash join, enquanto a Listagem 5b usa um Nested Loop Junte-se. Nota:A Listagem 5a retorna 4.000 linhas enquanto a Listagem 4b retorna 4 linhas.

Três etapas de ajuste de desempenho


A primeira otimização que fazemos é introduzir um índice (uma chave primária, para ser exato) no TranID coluna do TranLog tabela:
-- Listing 6: Create Primary Key
alter table TranLog add constraint PK_TranLog primary key clustered (TranID);

As Figuras 3 e 4 mostram que o SQL Server utiliza esse índice em ambas as consultas, fazendo uma varredura na Listagem 5a e uma busca na Listagem 5b.

Temos uma busca de índice na Listagem 5b. Isso acontece por causa da coluna envolvida no predicado da cláusula WHERE – TranID. É nessa coluna que aplicamos um índice.

Em seguida, introduzimos uma chave estrangeira no TranID coluna dos TranDetails tabela (Listagem 7).
-- Listing 7: Create Foreign Key
alter table TranDetails add constraint FK_TranDetails foreign key (TranID) references TranLog (TranID);

Isso não muda muito no plano de execução. A situação é praticamente a mesma mostrada anteriormente nas Figuras 3 e 4.

Em seguida, introduzimos um índice na coluna de chave estrangeira:
-- Listing 8: Create Index on Foreign Key
create index IX_TranDetails on TranDetails (TranID);

Essa ação altera drasticamente o plano de execução da Listagem 5b (consulte a Figura 6). Vemos mais índice procura acontecer. Além disso, observe a pesquisa RID na Figura 6.

As pesquisas RID em heaps geralmente acontecem na ausência de uma chave primária. Um heap é uma tabela sem chave primária.

Por fim, adicionamos uma chave primária ao TranDetails tabela. Isso elimina a varredura de tabela e a pesquisa de heap RID nas Listagens 5a e 5b, respectivamente (consulte as Figuras 7 e 8).
-- Listing 9: Create Primary Key on TranDetailsID
alter table TranDetails add constraint PK_TranDetails primary key clustered (TranDetailsID);

Conclusão


A melhoria de desempenho introduzida pelos índices é bem conhecida até mesmo pelo DBA iniciante. No entanto, queremos salientar que você precisa observar atentamente como as consultas usam índices.

Além disso, a ideia é estabelecer a solução no caso particular em que temos as consultas de junção entre Log de transações tabelas e Detalhes da transação mesas.

Geralmente faz sentido impor o relacionamento entre essas tabelas usando uma chave e introduzir índices nas colunas de chave primária e estrangeira.

Ao desenvolver aplicativos que usam esse design, os desenvolvedores devem ter em mente os índices e relacionamentos necessários no estágio de design. Ferramentas modernas para especialistas em SQL Server tornam esses requisitos muito mais fáceis de cumprir. Você pode criar o perfil de suas consultas usando a ferramenta especializada Query Profiler. Faz parte da solução profissional multifuncional dbForge Studio para SQL Server desenvolvida pela Devart para tornar a vida do DBA mais simples.