Olá! Seja bem-vindo!

Recentemente atuei em um projeto melhoria de performance, onde iniciei os trabalhos com um sistema rodando a 20 operações por minuto e terminei com 37 mil operações por minuto.

Surpreendentemente, as estratégias para chegar nesse resultado são bem menos complexas do que se pensou no início dos trabalhos, por isso hoje compartilharei contigo algumas delas nesse post/vídeo, onde veremos:

– Como evitar 2 erros fatais e corriqueiros quando estamos diante de problemas aparentemente grandes

– Como validar melhorias de performance comparativamente, para que a sua escolha seja mais efetiva

– Como criar testes automatizados que garantam a qualidade do seu trabalho

Assista a reprise da live aqui:

2 erros comuns ao enfrentarmos problemas grandes

Antes da parte técnica, quero compartilhar dois erros comuns que ocorrem em muitos projetos que participo. São questões conceituais que respaldam o conteúdo técnico que veremos.

#1: Pensar primeiro em soluções complexas

Quando nos deparamos com grandes desafios, tendemos a acreditar que são necessárias grandes soluções, mas nem sempre é o caso.

Quando cheguei ao projeto mencionado, percebi que essa tendência estava instalada: Analistas e arquitetos discutiam soluções robustas, envolvendo múltiplos servidores, operações assíncronas, protocolos de mensagens e outras tecnologias e estratégias que deixavam qualquer um exausto só de tentar explicar (ao menos eu fiquei).

Existem situações onde soluções complexas são necessárias, mas, veja você: A performance do sistema em questão terminou mudando de 20 para 37 mil operações por minuto sem qualquer solução complexa! Aliás: o ambiente que possuía 3 servidores, terminou sendo com apenas 2, e as operações que eram assíncronas para melhorar a performance, foram convertidas para síncronas!

Minha sugestão costumeiramente é “exaurir primeiro as alternativas simples e fundamentais antes de partirmos para algo complexo”. Elon Musk fala muito sobre isso “a construção de soluções grandes deve partir de premissas simples e fundamentais, as quais podemos ter certeza de que são verdade”. Em outras palavras, para construirmos grandes casas, precisamos de fundações sólidas!

Um alerta: esse tipo de afirmação/pergunta geralmente não é bem visto, especialmente pelos técnicos, que estão imergidos em acreditar na necessidade de coisas complexas para resolver problemas complexos! Por isso, antes de dizer qualquer coisa, minha recomendação é que tenha em mente propostas de solução parcialmente validadas…

Uma pergunta que costuma me ajudar a mapear e criar essas propostas é: “Quais são as alternativas simples, que dependem só de mim, e que pode ser testadas/validadas imediatamente?”

Para o projeto mencionado, essa pergunta me ajudou na montagem de uma POC (prova de conceito), a qual apresentei ao time como possível solução do caso. Com dados concretos respaldados pela filosofia que aqui conversamos, a direção do projeto mudou radical e imediatamente para “algo mais simples”.

#2: Implementar teorias sem validação prática

Outro engano comum, especialmente perpetrado por nós técnicos, é sair implementando soluções sem antes validá-las comparativamente e testa-las no mundo real.

Isso também ocorreu durante o projeto que mencionei, quando algumas implementações para melhorar a performance na realidade a pioraram!

É sério isso… eram propostas de soluções que funcionavam perfeitamente no campo teórico ou de testes unitários na máquina do desenvolvedor, mas que se provavam falhas no mundo real.

Einstain disse que “filosofia é fundamental, desse que seja útil na prática”.

Simples ou complexas, nossas propostas de soluções precisam ser antes validadas comparativamente de forma tangível, e isso faremos hoje com uma abordagem que procura ser inicialmente simples.

Medir viabiliza gerenciar

Existem inúmeras formas e ferramentas para medirmos performance de infra e objetos de programação, inclusive banco de dados. Algumas ferramentas que gosto muito são o Dynatrace e o New Relic, mas quando preciso medir scripts de forma mais individual e focada, costumo usar uma das estratégias exemplificadas abaixo:

--------------------------------------------------
-- Métodos para medição de performance - "O que não se mede, não se gerencia!" - Edwards Deming
--------------------------------------------------
use curso
go

-- #1: Execution Plan: CONTROL + M
select top 100 * from feriado
go

-- #2: Client Execution Statistics: SHIFT + ALT + S (Compara dados de cada execução. Para resetar "Menu Query > Reset Client Statistics")
select top 100 * from feriado
go

-- #3: set statistics io/time on-off
set statistics io on
set statistics time on
select top 100 * from feriado
set statistics io off
set statistics time off
go

-- #4: Getdate() - medidas em milisegundos (1s / 1000)
declare @dt_ini_rotina datetime = getdate()
select top 100 * from feriado
print 'Concluído: ' + convert(varchar, getdate() - @dt_ini_rotina, 114)
go

-- #5: Sysdatetime() - medidas em microsegundos (1s / 1 milhão)
declare @dt_ini_rotina datetime2 = sysdatetime()
select top 100 * from feriado
print concat('Concluído: ', right(concat('0', datediff(d, @dt_ini_rotina, sysdatetime())), 2), ' dias ', convert(varchar, convert(datetime, sysdatetime()) - convert(datetime, @dt_ini_rotina), 108), '.', right('000000' + convert(varchar, convert(bigint, datediff_big(microsecond, @dt_ini_rotina, sysdatetime()) % 1000000.0)), 6))
go

Cenários de melhoria

Uma vez que você entendeu como está funcionando uma query e onde estão os gargalos, pode começar a propor soluções para testes de acordo com o cenário.

Conceitualmente falando, melhorar a performance geralmente se trata de “reduzir o escopo operacional do software” ou “aumentar a capacidade operacional do hardware”.

Em banco de dados existem várias formas de reduzir o escopo operacional de uma transação sem alterar o código, aqui vão alguns exemplos:

1) Indexação: Índices reduzem o esforço computacional porque os dados estarão organizados, e o SQL poderá trabalhar apenas com o que interessa ao invés de tudo que há em uma determinada tabela, por exemplo.

2) Particionamento: Aliado a uma correta indexação, o particionamento pode reduzir dramaticamente o esforço operacional para processamento de suas querys em tabelas grandes.

3) Alocação de dados: Organizar seus dados de acordo com seu hardware é uma ótima estratégia para melhorar a performance de grandes volumes. Por exemplo: distribuindo uma tabela grande em vários datafiles que estejam alocados em discos distintos, permite a você aumentar significativamente a velocidade do sistema, pois você estará usando vários discos simultaneamente ao invés de apenas um.

4) Historificação e/ou expurgo: Historificação consiste em reduzir o tamanho das tabelas alocando dados antigos que são pouco utilizados em tabelas e bases separadas. Os dados em uso no momento ficam nas “tabelas quentes”, os dados antigos, em “tabelas frias”. Essa estratégia também é boa para reduzir custo de hw, pois os dados antigos podem ficar em hw menos qualificado, já que não são frequentemente acessados.

5) Tabelas em memória: Com o barateamento da memória RAM, alocar dados de forma permanente em memória tornou-se uma estratégia popular para melhorar a performance de dados que são constantemente acessados.

Propondo melhorias

Além dos exemplos citados acima, existem N formas de melhorar performance. Fundamental é entender bem o cenário e onde está o gargalo para que sua sugestão de mudança seja assertiva.

No caso do exemplo que mostrei no vídeo, algumas das estratégias aplicáveis estão descritas abaixo:

--------------------------------------------------
-- Cenários de melhoria
--------------------------------------------------
/*
#0: Manter "as is"
#1: Implementar índice nonclustered ou mudar índice clustered (nesse exemplo, mudar para DT_FERIADO)
#2: Tabela "in-memory" "as is"
#3: Tabela "in-memory" PK na coluna DT_FERIADO
#4: Tabela "in-memory" indice hash na coluna DT_FERIADO

OBS: Existem N estratégias de performance que não veremos aqui por não serem aplicáveis ao exemplo, ex:
Particionamento: Tabela muito pequena para fazer sentido.
Alocação em diferentes filegroups: Tabela muito pequena para fazer sentido.
Historificação de dados: Todos os dados são de produção (tabela de domínio). Não há dados históricos.
View indexada (trata-se de apenas 1 tabela, logo, faz mais sentido um indice direto a uma view indexada)
Etc...

*/

Validando performance

Um dos recursos mais valiosos e fáceis para medir a performance de suas propostas é a análise comparativa dos planos de execução (antes vs depois).

Para utilizar esse recurso é necessário executar todas as suas propostas de uma só vez, e isso não é difícil de fazer! Veja no exemplo abaixo como preparei e comparei 5 cenários de teste para a query que estamos analisando:

--------------------------------------------------
-- Requisitos do cenário in-memory
--------------------------------------------------
-- Adicionar FG com suporte para tabelas em memória:
alter database curso add filegroup Curso_MOD contains memory_optimized_data
go
alter database curso add file (name='Curso_MOD_01', filename='c:\tmp\Curso_MOD_01.ndf') to filegroup Curso_MOD
go

--------------------------------------------------
-- Montando cenários de teste
--------------------------------------------------
-- Apagar tabelas de teste caso existam
if object_id('feriado_p1') is not null drop table feriado_p1
if object_id('feriado_p2') is not null drop table feriado_p2
if object_id('feriado_p3') is not null drop table feriado_p3
if object_id('feriado_p4') is not null drop table feriado_p4
go

-- #1: Criar tabela de testes para proposta #1:
create table feriado_P1 (
id_feriado int not null,
dt_atualizacao datetime null,
tp_feriado char(1) not null,
dt_feriado datetime not null,
ds_feriado varchar(50) not null,
constraint pk_feriado_p1 primary key clustered (DT_FERIADO) -- Altero a PK para DT_FERIADO ao invés de ID_FERIADO
)

-- #2: Criar tabela de testes para proposta #2:
create table feriado_P2 (
id_feriado int not null,
dt_atualizacao datetime null,
tp_feriado char(1) not null,
dt_feriado datetime not null,
ds_feriado varchar(50) not null,
constraint pk_feriado_p2 primary key nonclustered (ID_FERIADO) -- in-memory não pode ter PK clustered. Na proposta 2, mantenho a pk na mesma coluna.
) WITH (MEMORY_OPTIMIZED=ON, DURABILITY=SCHEMA_AND_DATA) -- DURABILITY=SCHEMA_AND_DATA | SCHEMA_ONLY

-- #3: Criar tabela de testes para proposta #3:
create table feriado_P3 (
id_feriado int not null,
dt_atualizacao datetime null,
tp_feriado char(1) not null,
dt_feriado datetime not null,
ds_feriado varchar(50) not null,
constraint pk_feriado_p3 primary key nonclustered (DT_FERIADO) -- Altero a PK para DT_FERIADO ao invés de ID_FERIADO (mantenho no modelo in-memory)
) WITH (MEMORY_OPTIMIZED=ON, DURABILITY=SCHEMA_AND_DATA)
go

-- #4: Criar tabela de testes para proposta #4:
create table feriado_P4 (
id_feriado int not null,
dt_atualizacao datetime null,
tp_feriado char(1) not null,
dt_feriado datetime not null,
ds_feriado varchar(50) not null,
constraint pk_feriado_p4 primary key nonclustered (ID_FERIADO), -- Altero a PK para DT_FERIADO ao invés de ID_FERIADO (mantenho no modelo in-memory)
index ix_feriado_p4 hash (dt_feriado) with (bucket_count=1158) -- Para tabela estática defini o bucket-count = ao número de registros.
) WITH (MEMORY_OPTIMIZED=ON, DURABILITY=SCHEMA_AND_DATA)
go

-- Migrar os dados para as tabelas de teste:
insert into feriado_P1 select * from feriado
insert into feriado_P2 select * from feriado
insert into feriado_P3 select * from feriado
insert into feriado_P4 select * from feriado
go

--------------------------------------------------
-- Comparar a performance (tempo & plano de execução):
--------------------------------------------------
declare @dt_ini_rotina datetime2
declare @dt_ini date = '2010-01-01'
declare @dt_fim date = '2030-12-30'

-- #0 Cenário atual
set @dt_ini_rotina = sysdatetime()
select
count(dt_feriado)
from feriado
where
dt_feriado between @dt_ini and @dt_fim
and datepart(dw, dt_feriado) not in (7, 1) -- Não conta feriados em Sábados e Domíngo
print concat('Concluído: ', right(concat('0', datediff(d, @dt_ini_rotina, sysdatetime())), 2), ' dias ', convert(varchar, convert(datetime, sysdatetime()) - convert(datetime, @dt_ini_rotina), 108), '.', right('000000' + convert(varchar, convert(bigint, datediff_big(microsecond, @dt_ini_rotina, sysdatetime()) % 1000000.0)), 6))
-- #1 Cenário PK na DT_FERIADO
set @dt_ini_rotina = sysdatetime()
select
count(dt_feriado)
from feriado_P1 -- !!! MUDEI A QUERY APENAS AQUI !!!!
where
dt_feriado between @dt_ini and @dt_fim
and datepart(dw, dt_feriado) not in (7, 1) -- Não conta feriados em Sábados e Domíngo
print concat('Concluído: ', right(concat('0', datediff(d, @dt_ini_rotina, sysdatetime())), 2), ' dias ', convert(varchar, convert(datetime, sysdatetime()) - convert(datetime, @dt_ini_rotina), 108), '.', right('000000' + convert(varchar, convert(bigint, datediff_big(microsecond, @dt_ini_rotina, sysdatetime()) % 1000000.0)), 6))
-- #2 Cenário in-memory schema "as is"
set @dt_ini_rotina = sysdatetime()
select
count(dt_feriado)
from feriado_P2 -- !!! MUDEI A QUERY APENAS AQUI !!!!
where
dt_feriado between @dt_ini and @dt_fim
and datepart(dw, dt_feriado) not in (7, 1) -- Não conta feriados em Sábados e Domíngo
print concat('Concluído: ', right(concat('0', datediff(d, @dt_ini_rotina, sysdatetime())), 2), ' dias ', convert(varchar, convert(datetime, sysdatetime()) - convert(datetime, @dt_ini_rotina), 108), '.', right('000000' + convert(varchar, convert(bigint, datediff_big(microsecond, @dt_ini_rotina, sysdatetime()) % 1000000.0)), 6))
-- #3 Cenário in-memory PK na coluna DT_FERIADO
set @dt_ini_rotina = sysdatetime()
select
count(dt_feriado)
from feriado_P3 -- !!! MUDEI A QUERY APENAS AQUI !!!!
where
dt_feriado between @dt_ini and @dt_fim
and datepart(dw, dt_feriado) not in (7, 1) -- Não conta feriados em Sábados e Domíngo
print concat('Concluído: ', right(concat('0', datediff(d, @dt_ini_rotina, sysdatetime())), 2), ' dias ', convert(varchar, convert(datetime, sysdatetime()) - convert(datetime, @dt_ini_rotina), 108), '.', right('000000' + convert(varchar, convert(bigint, datediff_big(microsecond, @dt_ini_rotina, sysdatetime()) % 1000000.0)), 6))
-- #4 Cenário in-memory índice hash na coluna DT_FERIADO
set @dt_ini_rotina = sysdatetime()
select
count(dt_feriado)
from feriado_P4 -- !!! MUDEI A QUERY APENAS AQUI !!!!
where
dt_feriado between @dt_ini and @dt_fim
and datepart(dw, dt_feriado) not in (7, 1) -- Não conta feriados em Sábados e Domíngo
print concat('Concluído: ', right(concat('0', datediff(d, @dt_ini_rotina, sysdatetime())), 2), ' dias ', convert(varchar, convert(datetime, sysdatetime()) - convert(datetime, @dt_ini_rotina), 108), '.', right('000000' + convert(varchar, convert(bigint, datediff_big(microsecond, @dt_ini_rotina, sysdatetime()) % 1000000.0)), 6))
go

Validando funcionalidade

As soluções que vimos até aqui dependem apenas do SQL, mas, dependendo da situação, é comum chegarmos a um ponto onde “configuração do banco” e “arquitetura de hardware” simplesmente não comportam mais os objetivos.

Quando isso ocorre, é necessário partir para algo que os fabricantes de sw costumam resistir até o último momento…  recodificar o software.

Não é à toa a aversão a recodificar, pois implica em esforço, tempo, custos e riscos. Alguns desses elementos são inevitáveis, mas outros nós podemos reduzir, por exemplo, o risco de erros relacionados a recodificação da lógica.

Veja abaixo como costumo criar “testes automatizados” de objetos que recodifico. É uma técnica que me ajuda extremamente em projetos de software, pois permite que eu garanta a qualidade do que estou fazendo com um esforço relativamente pequeno.

Scripts completos da aula de hoje

[sociallocker]
Download dos arquivos
[/sociallocker]

Conclusão

Como vimos, existem diversas formas de medir e melhorar a performance de suas querys, mas estratégias simples podem levar a resultados expressivos.

Pensemos em soluções simples antes de complicar, e validemos bem o que estamos propondo antes de sair implementando, pois nem tudo que funciona na teoria é viável na prática.

Medir a performance e a funcionalidade comparativamente são dois pontos chave para você ter sucesso com suas entregas e se destacar da multidão profissionalmente, e hoje vimos tecnicamente que isso é simples de fazer, desde que você aborde a questão estrategicamente.

Abraço do seu amigo Josué

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *