Postgres – Criar estatísticas

O Postgres é “tão evoluído” e blá blá blá, mas não consegue ter uma rotina de criação automática de estatísticas. Parece que voltamos para antes dos anos 2000.

Bom, não vou explicar a importância da criação de estatísticas, isso você já deveria saber, se não sabe, pergunta para sua IA favorita.

A código abaixo vai ler a tabela pg_stat_statements e, a partir dela, eu tento fazer um monte de regex para separar das querys a parte do SARG para criar as estatísticas.

É preciso adicionar a extensão pg_stat_statements:

create extension pg_stat_statements;

Dependendo do seu workload logo em seguida você já vai ter acesso a alguma coisa, mas para ter dados melhores, é melhor deixar o tempo passar para acumular mais informações.

Depois de algum tempo, quando você rodar o script abaixo, deve ter um resultado mais interessante para criar as estatísticas:

WITH normalized AS (SELECT queryid
                         , query
                         -- Remove DO $ ... $ e normaliza espaços
                         , lower(regexp_replace(
            regexp_replace(query, '^do \$\$|\\$\$;$', '', 'gi'),
            '\s+', ' ', 'g'
                                 )) AS norm_query
                         , total_exec_time
                         , calls
                    FROM pg_stat_statements
                    WHERE query ILIKE 'select%'
                      AND calls > 10
                      AND query ~* ' where ')
   , tables AS (SELECT n.queryid
                     , n.query
                     , n.norm_query
                     , n.total_exec_time
                     , n.calls
                     -- Captura tabela e alias
                     , (regexp_match(n.norm_query,
                                     '(?:from|join)\s+([a-z0-9_\.]+)(?:\s+as\s+|\s+)?([a-z0-9_]+)?'))[1] AS table_obj_full
                     , (regexp_match(n.norm_query,
                                     '(?:from|join)\s+([a-z0-9_\.]+)(?:\s+as\s+|\s+)?([a-z0-9_]+)?'))[2] AS table_alias
                     , COALESCE(
            (regexp_match(n.norm_query, '(?:from|join)\s+([a-z0-9_\.]+)(?:\s+as\s+|\s+)?([a-z0-9_]+)'))[2],
            split_part((regexp_match(n.norm_query, '(?:from|join)\s+([a-z0-9_\.]+)'))[1], '.', 2)
                       )                                                                                 AS main_identifier_raw
                     , split_part(
            lower(
                    regexp_replace(
                            (regexp_match(n.norm_query, '(?:from|join)\s+([a-z0-9_\.]+)'))[1],
                            '[^a-z0-9_\.]',
                            '',
                            'g'
                    )
            ),
            '.',
            CASE WHEN (regexp_match(n.norm_query, '(?:from|join)\s+([a-z0-9_\.]+)'))[1] LIKE '%.%' THEN 2 ELSE 1 END
                       )                                                                                 AS table_name
                FROM normalized n
                WHERE (regexp_match(n.norm_query, '(?:from|join)\s+([a-z0-9_\.]+)'))[1] IS NOT NULL)
   , where_clauses AS (SELECT t.queryid
                            , t.table_name
                            , lower(t.main_identifier_raw) AS main_identifier
                            , (regexp_match(
            t.norm_query,
            'where\s+(.*?)(?:\sgroup by|\sorder by|\slimit|;|$)'
                               ))[1]                       AS where_block
                            , t.total_exec_time
                            , t.calls
                       FROM tables t)
   , where_columns AS (SELECT wc.table_name
                            , wc.main_identifier
                            , CASE
                                  WHEN rm.m[1] LIKE '%.%' THEN lower(split_part(rm.m[1], '.', 1))
                                  ELSE NULL END                                 AS column_prefix
                            , rm.m[1]                                           AS full_column_name
                            , regexp_replace(rm.m[1], '^[a-z0-9_]+\.', '', 'i') AS column_name
                            , wc.total_exec_time
                       FROM where_clauses wc
                                CROSS JOIN LATERAL regexp_matches(
                               wc.where_block,
                               '([a-z_][a-z0-9_\.]*)\s*(=|>|<|>=|<=|<>|in\b|like\b)',
                               'gi'
                                                   ) AS rm(m))
   , distinct_columns AS (SELECT table_name
                               , column_name
                               , SUM(total_exec_time) AS total_cost
                          FROM where_columns
                          WHERE column_name IS NOT NULL
                            AND (
                              column_prefix IS NULL
                                  OR column_prefix = main_identifier
                                  OR column_prefix = table_name
                              )
                          GROUP BY table_name, column_name)
   , validated_columns AS (SELECT dc.table_name
                                , dc.column_name
                                , dc.total_cost
                           FROM distinct_columns dc
                                    JOIN information_schema.columns isc
                                         ON lower(isc.table_name) = lower(dc.table_name)
                                             AND lower(isc.column_name) = lower(dc.column_name)
                           WHERE dc.table_name NOT LIKE 'pg_%')
   , family AS (SELECT vc.table_name
                     , (SELECT array_agg(c)
                        FROM (SELECT column_name AS c
                              FROM validated_columns sub
                              WHERE sub.table_name = vc.table_name
                              ORDER BY total_cost DESC
                              LIMIT 8) sub2)       AS columns
                     , COUNT(DISTINCT column_name) AS occurrences
                     , SUM(total_cost)             AS total_table_cost
                FROM validated_columns vc
                GROUP BY vc.table_name
                HAVING COUNT(DISTINCT column_name) > 1)
   , correlation_estimate AS (SELECT f.*
                                   , (array_length(f.columns, 1) ^ 1.3) * ln(total_table_cost + 5) AS score
                              FROM family f)
   , existing_ext_stats AS (SELECT cls.relname                                         AS table_name
                                 , st.stxname                                          AS stat_name
                                 , array_agg(att.attname ORDER BY att.attname)::text[] AS stat_columns
                            FROM pg_statistic_ext st
                                     JOIN pg_class cls ON cls.oid = st.stxrelid
                                     JOIN pg_attribute att
                                          ON att.attrelid = cls.oid
                                              AND att.attnum = ANY (st.stxkeys)
                            GROUP BY cls.relname, st.stxname)
   , dedup AS (SELECT ce.*
                    , 's_' || regexp_replace(ce.table_name, '[^a-z0-9_]', '', 'g') ||
                      '_' || substr(md5(array_to_string(ce.columns, ',')), 1, 8) AS stat_name
                    , EXISTS (SELECT 1
                              FROM existing_ext_stats es
                              WHERE es.table_name = ce.table_name
                                AND es.stat_columns = ce.columns::text[])        AS already_exists
               FROM correlation_estimate ce)

SELECT table_name
     , columns
     , occurrences
     , round(score::numeric, 2)                                AS score
     , stat_name
     , 'CREATE STATISTICS IF NOT EXISTS ' ||
       quote_ident(stat_name) ||
       ' ON ' ||
       array_to_string(
               ARRAY(SELECT quote_ident(c) FROM unnest(columns) c), ', '
       ) ||
       ' FROM ' ||
       quote_ident(table_name) ||
       ';'                                                     AS suggested_cmd
     , 'Filtros mais custosos sobre (' ||
       array_to_string(columns, ', ') ||
       ') em ' || table_name ||
       '. Ocorrências: ' || occurrences ||
       '. Custo total estimado: ' || total_table_cost || 'ms.' AS justification
FROM dedup
WHERE NOT already_exists
  AND table_name NOT LIKE 'pg_%'
  AND table_name NOT IN ('table_constraints', 'columns', 'tables')
  AND array_length(columns, 1) > 1
ORDER BY score DESC, total_table_cost DESC, occurrences DESC;

Ordem da chave primária

A ideia desses 2 scripts é fazer uma simples análise e sugerir uma possível melhoria na ordem da chave primária de uma tabela específica.

Estes scripts não são para serem seguidos a ferro e fogo, existem outros fatores para serem considerados na ordem de uma PK, mas para quem não tem nada, e quer ter pelo menos uma ideia para onde ir montei eles como procedure para o SQL e como função para o Postgresql.

Versão SQL Server:

ALTER PROCEDURE usp_SugerirNovaOrdemChavePrimaria
    @SchemaName NVARCHAR(128),
    @TableName NVARCHAR(128)
AS
BEGIN
	SET ARITHABORT OFF 
	SET ANSI_WARNINGS OFF
    DECLARE @ColumnName NVARCHAR(128);
    DECLARE @Sql NVARCHAR(MAX) = '';
    DECLARE @OrderSuggestions NVARCHAR(MAX) = '';
    DECLARE @DynamicSQL NVARCHAR(MAX);
    DECLARE @Densidade FLOAT;
    DECLARE @OrderTable TABLE (ColumnName NVARCHAR(128), Densidade FLOAT);
    DECLARE @CurrentOrder NVARCHAR(MAX) = '';

    -- Cursor para iterar sobre as colunas da chave primária
    DECLARE ColumnCursor CURSOR FOR
        SELECT COL_NAME(ic.object_id, ic.column_id) 
        FROM sys.indexes AS i 
        INNER JOIN sys.index_columns AS ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id 
        WHERE i.is_primary_key = 1 
        AND OBJECT_NAME(ic.object_id) = @TableName;

    OPEN ColumnCursor;
    FETCH NEXT FROM ColumnCursor INTO @ColumnName;

    WHILE @@FETCH_STATUS = 0
    BEGIN
        -- Constrói e executa a consulta SQL dinâmica para calcular a densidade para cada coluna
        SET @DynamicSQL = 'SELECT @DensidadeOUT = (COUNT(DISTINCT ' + QUOTENAME(@ColumnName) + ') * 1.0 / COUNT(*)) FROM ' + QUOTENAME(@SchemaName) + '.' + QUOTENAME(@TableName);
        EXEC sp_executesql @DynamicSQL, N'@DensidadeOUT FLOAT OUTPUT', @Densidade OUTPUT;

        -- Adiciona os resultados em uma tabela temporária
        INSERT INTO @OrderTable (ColumnName, Densidade) VALUES (@ColumnName, @Densidade);

        -- Constrói a ordem atual
        SET @CurrentOrder += @ColumnName + ', ';

        FETCH NEXT FROM ColumnCursor INTO @ColumnName;
    END

    CLOSE ColumnCursor;
    DEALLOCATE ColumnCursor;

    -- Remove a última vírgula e espaço da ordem atual
    IF LEN(@CurrentOrder) > 0
    BEGIN
        SET @CurrentOrder = LEFT(@CurrentOrder, LEN(@CurrentOrder) - 1);
    END

    -- Constrói a sugestão de ordem com base na densidade
    SELECT @OrderSuggestions += ColumnName + ', '
    FROM @OrderTable
    ORDER BY Densidade ASC, ColumnName;

    -- Remove a última vírgula e espaço
    IF LEN(@OrderSuggestions) > 0
    BEGIN
        SET @OrderSuggestions = LEFT(@OrderSuggestions, LEN(@OrderSuggestions) - 1);
    END

    -- Compara a ordem atual com a sugerida
    IF @CurrentOrder = @OrderSuggestions
    BEGIN
        SELECT @TableName as [Object], 'A ordem atual já é a melhor.' AS SuggestedOrder;
    END
    ELSE
    BEGIN
        -- Retorna a sugestão de ordem
        SELECT @TableName as [Object], @OrderSuggestions AS SuggestedOrder;
    END
END

Versão para Postgresql:

CREATE OR REPLACE FUNCTION mc1_sugerir_nova_ordem_chave_primaria(schema_name text, nome_tabela text)
RETURNS text AS $$
DECLARE
    coluna_atual record;
    ordem_sugerida text := '';
    ordem_atual text := '';
    query text;
BEGIN
    -- Prepara a query para obter a ordem atual das colunas da chave primária
    FOR coluna_atual IN
        SELECT kcu.column_name
        FROM information_schema.table_constraints AS tc
        JOIN information_schema.key_column_usage AS kcu
            ON tc.constraint_name = kcu.constraint_name
            AND tc.table_schema = kcu.table_schema
        WHERE tc.constraint_type = 'PRIMARY KEY'
            AND tc.table_name = nome_tabela
            AND tc.table_schema = schema_name
        ORDER BY kcu.ordinal_position
    LOOP
        ordem_atual := ordem_atual || coluna_atual.column_name || ', ';
    END LOOP;

    -- Remove a última vírgula e espaço da ordem atual
    IF LENGTH(ordem_atual) > 0 THEN
        ordem_atual := substr(ordem_atual, 1, LENGTH(ordem_atual) - 2);
    END IF;

    -- Prepara a query para calcular a densidade das colunas da chave primária
    query := format($f$
        SELECT 
            kcu.column_name,
            (COUNT(DISTINCT %I) * 1.0 / COUNT(*)) AS densidade
        FROM 
            information_schema.table_constraints AS tc
        JOIN 
            information_schema.key_column_usage AS kcu
            ON tc.constraint_name = kcu.constraint_name
            AND tc.table_schema = kcu.table_schema
        JOIN 
            %I.%I AS t
            ON true
        WHERE 
            tc.constraint_type = 'PRIMARY KEY'
            AND tc.table_name = %L
            AND tc.table_schema = %L
        GROUP BY 
            kcu.column_name
        ORDER BY 
            densidade DESC, kcu.column_name
    $f$, 'column_name', schema_name, nome_tabela, nome_tabela, schema_name);

    -- Executa a consulta e processa os resultados
    FOR coluna_atual IN EXECUTE query
    LOOP
        ordem_sugerida := ordem_sugerida || coluna_atual.column_name || ', ';
    END LOOP;

    -- Remove a última vírgula e espaço
    IF LENGTH(ordem_sugerida) > 0 THEN
        ordem_sugerida := substr(ordem_sugerida, 1, LENGTH(ordem_sugerida) - 2);
    END IF;

    -- Compara a ordem atual com a sugerida
    IF ordem_atual = ordem_sugerida THEN
        RETURN 'A ordem atual já é a melhor.';
    ELSE
        RETURN ordem_sugerida;
    END IF;
END;
$$ LANGUAGE plpgsql;

SQL na Caixinha

Que o SQL pode rodar em container já não é novidade tem um tempinho.

A facilidade que isso nos trás para testar recursos, novidades, configurações, bugs, etc. ajudou demais.

Só o trabalho de subir um SO, configurar todo o SO, atualizações do SO, baixar a instalação do SQL, todo o processo de instalação, atualização, configuração já cansa só de lembrar.

Tá certo que com a vantagem na nuvem podemos subir qualquer configuração a qualquer momento, só dependendo do limite do cartão de crédito, mas com o Docker, da pra fazer basicamente a mesma coisa sem precisar de uma conta em alguma nuvem, sem ter que ficar instalando um monte de binário com um monte de biblioteca, não se preocupando se está no patch certo do SO, etc.

Basicamente com duas linhas de comando você consegue “rodar” qualquer SQL Server do 17 até o 22 em qualquer cumulative update que houve nesse meio tempo.

Para começar, instale o Docker Desktop (https://www.docker.com/get-started/);

Após alguns restarts e atualizações você deve ter ele pronto no seu PC.

Agora, no Terminal, PowerShell, CMD digite o comando abaixo:

docker pull mcr.microsoft.com/mssql/server

Espere ele carregar algumas configurações

Em seguida vem a mágica com esse segundo comando:

docker run -e "ACCEPT_EULA=Y" -e "SA_PASSWORD=yourStrong(!)Password" -p 1433:1433 -d mcr.microsoft.com/mssql/server:2022-latest

E pronto, basicamente só isso e você vai ter um SQL Server Developer Edition 2022 rodando no seu PC

Claro que tem seus detalhes, nessa configuração simples tudo o que acontece no docker fica dentro do docker, se você apagar o container todas as bases que você criou, registros, etc. serão apagadas, o backup também conta.

Por padrão ele não integra com autenticação windows.

Se você procurar nos serviços ele não aparece listado.

Basicamente para conectar é seu hostname e a porta 1433 com usuário SA e a senha digitada ali em cima.

Caso precise da lista de todas as releases que você pode subir com o Docker a lista encontra-se aqui (https://hub.docker.com/_/microsoft-mssql-server).

AWS – Redshift – Lock e Block

Por incrível que pareça o Redshift sofre com problemas de Lock e Block da mesma forma que um banco transacional qualquer.

Como qualquer post sobre nuvem, até o momento dessa publicação, o Redshift não tem uma interface que monitora Lock e Block, ela monitora conexões ativas, querys em execução mas não lock e block.

A query para monitorar o Redshift é a seguinte:

select a.txn_owner, a.txn_db, a.xid, a.pid, a.txn_start, a.lock_mode, a.relation as table_id,nvl(trim(c."name"),d.relname) as tablename, a.granted,b.pid as blocking_pid ,datediff(s,a.txn_start,getdate())/86400||' days '||datediff(s,a.txn_start,getdate())%86400/3600||' hrs '||datediff(s,a.txn_start,getdate())%3600/60||' mins '||datediff(s,a.txn_start,getdate())%60||' secs' as txn_duration
from svv_transactions a 
left join (select pid,relation,granted from pg_locks group by 1,2,3) b 
on a.relation=b.relation and a.granted='f' and b.granted='t' 
left join (select * from stv_tbl_perm where slice=0) c 
on a.relation=c.id 
left join pg_class d on a.relation=d.oid
where  a.relation is not null;

e para dar um kill no processo

select pg_terminate_backend(PID);

o resultado deve vir como “1”

No link abaixo tem mais informações:

https://aws.amazon.com/pt/premiumsupport/knowledge-center/prevent-locks-blocking-queries-redshift/

Liberar toda a memória do servidor

Todos sabemos que o SQL é um consumidor de memória frenético, quanto mais memória disponível mais memória ele vai reservar para ele.

O que é um desenho “by default”, ele sempre fará isso afinal de contas ele precisa alocar as páginas de dados do seu banco em algum lugar.

Para resolver todos os seus problemas, existe uma forma de liberar toda a memória disponível de uma só vez do seu servidor e não é parando o serviço do SQL.

Para isso, você vai precisar o Visual Studio instalado, vamos criar um novo projeto dele…

Importante! Abra o Visual Studio como administrador !

Novo projeto de linha de comando

Escreva o nome que quiser para o app

Copie e cole o código abaixo no projeto:

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;

public class CriticalProcess
{
    [DllImport("ntdll.dll", SetLastError = true)]
    private static extern int NtSetInformationProcess(IntPtr hProcess, int processInformationClass, ref int processInformation, int processInformationLength);

    static void Main(string[] args)
    {
        int isCritical = 1;  // queremos que ele seja um processo crítico
        int BreakOnTermination = 0x1D;  // valor para BreakOnTermination (flag)

        Process.EnterDebugMode();  //acquire Debug Privileges

        // configurando o BreakOnTermination = 1 para o processo ativo
        NtSetInformationProcess(Process.GetCurrentProcess().Handle, BreakOnTermination, ref isCritical, sizeof(int));
    }
}

Se tudo ocorrer como esperado, dependendo da quantidade de memória do seu servidor isso pode demorar de alguns segundos a algumas horas.

Por mais que tenhamos criado uma aplicação de linha de comando a primeira parte do processo é bem gráfica e todos já tiveram o grande prazer de conhecer:

Ele vai gerar um DUMP de toda a memória para o arquivo de paginação e depois que a maquina reiniciar ele vai copiar esse arquivo de paginação para um arquivo chamado memory.dump

É só isso,,, execução e queda,,,

Agora falando sério: NUNCA !!!! JAMAIS !!!!! Simplesmente pegue o código de qualquer coisa que você encontra na internet e saia executando sem antes entender o que ele faz.

Esse exemplo é bem ridículo, mas imagina um script que você leu o por alto achando que vai resolver todos os seus problemas de backup, ou de fragmentação de índice e descobre que no meio tem um sp_msforeach_table com um sp_msforeach_db que trunca as tabelas, ou pior, alguém cria uma chave de criptografia e habilita TDE nas suas bases e depois força a remoção da chave,,,, a culpa é tão e somente sua! Você é o DBA é sua responsabilidade preservar os dados.

Tenha discernimento com o que você copia da internet e de onde copia essas informações.

SEMPRE LEIA e NUNCA EXECUTE DIRETAMENTE EM PRODUÇÃO !!!

Apagar arquivos de backup duplicados

Imagine o seguinte cenário:

Você tem sua rotina de backup (FULL, DIFF, LOG) que gera os arquivos de saída como por exemplo BKPFULL_BASE_XPTO_01_DE_04_20181105.bak e coisas parecidas com isso.

Sua ferramenta de backup copia esses arquivos para uma área de staging todos os dias, marcando os arquivo com o bit de arquivado, no dia seguinte você tem um step do job que procura por esses arquivos e apaga ele, afinal, já foram marcados como arquivados pelo software de backup.

Em um certo momento, alguma coisa aconteceu nessa rotina da ferramenta e ela não marcou os arquivos ou simplesmente não rodou.

Para não ficar sem espaço em disco você resolve apagar o arquivo mais antigo do backup deixando pelo menos o mais recente no disco, para um ambiente com poucas bases isso é tranquilo, imagine isso para um ambiente com algumas centenas de bases, em um final de semana prolongado, algumas bases com 3 ou 4 arquivos de backup, outras com apenas 1 arquivo.

O PowerShell abaixo faz um parse no nome do arquivo para agrupar pelo tipo do backup e o nome do banco, procura onde tem mais de uma entrada (imaginando que você separa isso por discos), remove do resultado o mais recente e apaga os mais antigos.

O script não é perfeito, ainda faltam alguns detalhes à serem melhorados, mas já é uma ajuda em casos como esse:

 


Get-ChildItem "X:\Backup\Disk02\" -file | where Name -match "._(\d{4})(\d{2})(\d{2})" | Where-Object {$_.Attributes -Eq "Normal"} | #Esse Atributo é o que o software de backup marca como retido, retire este Where-Object caso queira desconsiderar isso
select fullname, #@{N="DtFile";E={[DateTime]$_.BaseName.substring($_.BaseName.length -10).replace("_", "-")}},
@{N="FileWithoutDate";E={$_.BaseName.substring(0, $_.BaseName.length -18)}} |
group FileWithoutDate |
where Count -GE 2 |
%{ $_.Group | sort fullname,DtFile -Descending | select -skip 1} | %{Remove-Item $_.FullName -WhatIf}

Bah! – SQLSaturday #744 – Caxias do Sul Tchê !

Bah! Tchê! Vamos ver se eles realmente fazem um churrasco melhor que o paulista ou é só intriga da oposição.

No dia 23/06/2018 Haverá um novo evento do SQL Saturday, desta vez em Caxias do Sul/RS.

É com um prazer inenarrável em estar presente para mais um evento da comunidade e ainda mais como palestrante.

Só tem uma coisa mais legal que a minha palestra,,, o café,,, esqueça as outras, mantenha o foco,,,

Vamos conversar sobre formas “erradas” de como fazer as coisas no SQL Server.

Instalar o MSSQL-CLI

Já tem um tempo que a Microsoft disponibilizou via github outro cliente para administrar o SQL Server o MSSQL-CLI.

Esse cliente, como o SQL Ops Studio, vem com o conceito de multi plataforma (Linux, Windows e Mac).

Mas ao invés de focar em uma interface gráfica ele é a evolução do SQLCMD (para a alegria da turma do Shell).

Para instalar o cliente o processo é muito simples, isso se você não tiver um proxy na sua empresa, antes de tudo você precisa instalar o Phyton.

Após a instalação abra o prompt como administrador e digite:

pip install mssql-cli

Espere alguns minutos (dependendo da sua qualidade de Internet) e ele será instalado, você não tem opção de personalização da instalação.

Para abrir o cliente digite:

MSSQL-CLI -Sservidor

Existem outros parâmetros que você pode usar.

Já para quem está atrás de um firewall,,, lá vem a novela…

Primeiro que você precisa saber qual o endereço do seu proxy e porta.

Segundo que se você seguir a documentação pode acabar tendo alguns problemas com detalhes…

Na documentação do pip ele indica que você precisa passar o parâmetro – – proxy=username:password@proxy:port

Se você prestou atenção o caracter de separação do usuário da senha é “:” e o de separação entre a senha e o servidor é o “@”, logo, se sua senha possuir um desses 2 caracteres Vc vai receber um monte de erro estranho.

Para resolver isso configure 2 variáveis de ambiente a http_proxy e https_proxy da seguinte forma:

Tudo no prompt de comando

set http_proxy=dominio\usuário:senh%40@servidor:porta

set https_proxy=dominio\usuário:senh%40@servidor:porta

Você percebeu que o @ da senha ficou com %40?

A tabela é a seguinte :

@ – – > %40

$–>%24

!–>%21

Aí o resto é a mesma coisa para instalar :

pip install mssql-cli

Espero que tenha ajudo quem está tentando instalar através de um proxy.

Esse cliente tem um bom potencial para substituir o sqlcmd mas ainda tem muita coisa para melhorar…