Postgres – Pooler de conexão com Cache

Postgres é sensacional, é parrudo e todo mundo sabe que é o “queridinho” para dados hoje em dia. Mas ele não faz milagre. Se você tem uma aplicação que espanca o banco com queries repetitivas (aqueles SELECTs que não mudam quase nunca, mas rodam milhares de vezes por segundo), o seu I/O vai pro espaço, a CPU sobe e a latência vira um pesadelo.

Recentemente, trabalhei em uma adaptação do PG_Dog (um proxy/pooler focado em Postgres) para resolver exatamente esse problema. O “pulo do gato”? Coloquei uma camada de cache chave-valor usando #Redis em paralelo ao pooler.

Por que colocar cache no Pooler?

Se a sua aplicação é legada ou se você tem um time de dev que não quer (ou não pode) mexer no código para implementar cache, você resolve isso no “meio do caminho”. O PG_Dog intercepta a query, vê se ela já foi feita antes e entrega o resultado na velocidade da memória.

A vantagem de usar Redis com Sharding em paralelo é que a gente não cria um novo gargalo. Se um nó de Redis ficar cheio ou sobrecarregado, a carga está distribuída entre vários shards.

Como a mágica acontece?

Basicamente, a estrutura funciona assim:

  1. Identificação: A aplicação manda a query pro PG_Dog.
  2. Hashing: O PG_Dog gera um hash único baseado no SQL e nos parâmetros. Esse hash é a chave.
  3. Check de Cache: Ele consulta os #shards do Redis em paralelo.
  4. Cache Hit: Se o dado estiver lá, ele devolve pro usuário em <5ms. O Postgres nem acorda.
  5. Cache Miss: Se não estiver, ele executa no Postgres, popula o Redis para a próxima e entrega o resultado.

Invalidação?

Claro que temos, funciona assim:

  1. Identificação: A aplicação manda o DML ou DDL pro PG_Dog.
  2. Check de Cache: Ele consulta os #shards do Redis em paralelo.
  3. Cache Delete Se a tabela estiver lá ele deleta a chave ou chaves envolvidas.
  4. No Commit: Se uma transação for aberta mas não comitada eu não limpo o cache.

Por que cache externo?

A vantagem de ter um pooler é poder escalar ele horizontalmente, afinal, não queremos gerar um ponto único de falha no ambiente.

Mas, estalar ele horizontal implica em trazer um outro problema para a mesa, um select pode criar um cache para aquele select, mas e se um DML ou DDL usar um outro pooler para fazer a alteração, como invalidar esse cache para evitar um falso positivo? por isso a ideia de usar um Redis (ou qualquer outro cache que use a mesma tecnologia) externo ao pooler.

O container do pooler fica pequeno e você escala o cache da melhor forma possível. (Shard, Cluster, Single, aí é com você).

    Configuração (Mão na massa)

    Não adianta só falar, tem que mostrar como configura. No arquivo de configuração do seu PG_Dog (que agora aceita múltiplos backends de cache), a coisa fica mais ou menos assim:

    YAML

    # Exemplo de config do PG_Dog com Redis Sharding
    [result_cache]
    enabled = true
    # Redis/Valkey/Dragonfly connection URL.
    redis_url = "redis://127.0.0.1:6379"
    # TTL (seconds). If omitted, PgDog applies a default.
    expire_seconds = 30
    # Don't cache very large results.
    max_entry_bytes = 524288
    # Redis key prefix.
    key_prefix = "pgdog:result_cache"
    # Optional allow/deny lists (regex) to control what gets cached.
    # Unsafe lists take precedence over safe lists.
    cache_safe_schema_list = []
    cache_unsafe_schema_list = []
    cache_safe_table_list = []
    cache_unsafe_table_list = []
    
    

    Na configuração acima você pode ver que da pra personalizar como no PG_Pool2, não fazer cache de tabelas específicas ou de schemas específicos, e por sinal, da pra usar regex caso precise.

    Conclusão

    Essa adaptação transforma o PG_Dog em uma ferramenta de aceleração ativa. Você ganha fôlego no banco de dados principal, economiza em instância de nuvem (RDS/CloudSQL).

    O código está aberto para quem quiser testar, quebrar ou melhorar lá no meu GitHub:

    👉 github.com/bigleka/pgdog

    Ainda estou em fase de testes totalmente Alfa, se alguém tiver coragem de começar a testar e ir encontrando erros é só avisar.

    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.