Deixe-me lembrá-lo de que examinamos os problemas relacionados ao
isolamento , fizemos uma digressão sobre a
organização de dados em um nível baixo e conversamos detalhadamente
sobre as versões de linha e como os
instantâneos são obtidos a partir das versões.
Hoje, trataremos de dois problemas bastante próximos:
limpeza entre páginas e
atualizações HOT . Ambos os mecanismos podem ser classificados como otimizações; eles são importantes, mas quase não são abordados na documentação do usuário.
Limpeza na página com atualizações regulares
Ao acessar a página - durante a atualização e a leitura - pode ocorrer uma rápida limpeza intra-página se o PostgreSQL entender que a página está ficando sem espaço. Isso ocorre em dois casos.
- Uma atualização realizada anteriormente nesta página (UPDATE) não encontrou espaço suficiente para colocar uma nova versão da linha na mesma página. Essa situação é lembrada no título da página e na próxima vez que a página for limpa.
- A página é preenchida mais do que no fator de preenchimento. Nesse caso, a limpeza ocorre imediatamente, sem atrasar a próxima vez.
O fator de preenchimento é um parâmetro de armazenamento que pode ser definido para a tabela (e para o índice). O PostgreSQL insere uma nova linha (INSERT) na página apenas se esta for menor que o percentual de preenchimento. O espaço restante é reservado para novas versões de cadeias resultantes de atualizações (UPDATE). O valor padrão para tabelas é 100, ou seja, o espaço não é reservado (e o valor para índices é 90).
A limpeza dentro da página remove versões de linhas que não são visíveis em nenhuma imagem (localizada além do "horizonte de eventos" do banco de dados, falamos sobre a
última vez ), mas funciona estritamente na mesma página tabular. Os ponteiros para versões depuradas de strings não são liberados, porque podem ser referenciados a partir de índices e o índice é outra página. A limpeza na página nunca ultrapassa uma página tabular, mas é muito rápida.
Pelas mesmas razões, o mapa de espaço livre não é atualizado; também economiza espaço para atualizações, não para inserções. O mapa de visibilidade também não é atualizado.
O fato de uma página poder ser limpa durante a leitura significa que uma solicitação de leitura (SELECT) pode fazer com que as páginas sejam alteradas. Esse é outro caso, além da alteração previamente adiada dos bits de dica.
Vamos ver como isso funciona, usando um exemplo. Crie uma tabela e índices nas duas colunas.
=> CREATE TABLE hot(id integer, s char(2000)) WITH (fillfactor = 75); => CREATE INDEX hot_id ON hot(id); => CREATE INDEX hot_s ON hot(s);
Se apenas as letras latinas forem armazenadas na coluna s, cada versão da linha ocupará 2004 bytes mais 24 bytes do cabeçalho. Definimos o parâmetro de armazenamento do fator de preenchimento para 75% - haverá espaço suficiente para três linhas.
Por conveniência, recriamos uma função já familiar, complementando a saída com dois campos:
=> CREATE FUNCTION heap_page(relname text, pageno integer) RETURNS TABLE(ctid tid, state text, xmin text, xmax text, hhu text, hot text, t_ctid tid) AS $$ SELECT (pageno,lp)::text::tid AS ctid, CASE lp_flags WHEN 0 THEN 'unused' WHEN 1 THEN 'normal' WHEN 2 THEN 'redirect to '||lp_off WHEN 3 THEN 'dead' END AS state, t_xmin || CASE WHEN (t_infomask & 256) > 0 THEN ' (c)' WHEN (t_infomask & 512) > 0 THEN ' (a)' ELSE '' END AS xmin, t_xmax || CASE WHEN (t_infomask & 1024) > 0 THEN ' (c)' WHEN (t_infomask & 2048) > 0 THEN ' (a)' ELSE '' END AS xmax, CASE WHEN (t_infomask2 & 16384) > 0 THEN 't' END AS hhu, CASE WHEN (t_infomask2 & 32768) > 0 THEN 't' END AS hot, t_ctid FROM heap_page_items(get_raw_page(relname,pageno)) ORDER BY lp; $$ LANGUAGE SQL;
E vamos criar uma função para olhar dentro da página de índice:
=> CREATE FUNCTION index_page(relname text, pageno integer) RETURNS TABLE(itemoffset smallint, ctid tid) AS $$ SELECT itemoffset, ctid FROM bt_page_items(relname,pageno); $$ LANGUAGE SQL;
Vamos verificar como funciona a limpeza dentro da página. Para fazer isso, insira uma linha e altere-a várias vezes:
=> INSERT INTO hot VALUES (1, 'A'); => UPDATE hot SET s = 'B'; => UPDATE hot SET s = 'C'; => UPDATE hot SET s = 'D';
Existem quatro versões da linha na página:
=> SELECT * FROM heap_page('hot',0);
ctid | state | xmin | xmax | hhu | hot | t_ctid -------+--------+----------+----------+-----+-----+-------- (0,1) | normal | 3979 (c) | 3980 (c) | | | (0,2) (0,2) | normal | 3980 (c) | 3981 (c) | | | (0,3) (0,3) | normal | 3981 (c) | 3982 | | | (0,4) (0,4) | normal | 3982 | 0 (a) | | | (0,4) (4 rows)
Como esperado, acabamos de exceder o limite do fator de preenchimento. Isso é indicado pela diferença entre o tamanho da página e os valores superiores: excede o limite de 75% do tamanho da página, que é 6144 bytes.
=> SELECT lower, upper, pagesize FROM page_header(get_raw_page('hot',0));
lower | upper | pagesize -------+-------+---------- 40 | 64 | 8192 (1 row)
Portanto, na próxima vez que você acessar a página, uma limpeza na página deve ocorrer. Veja isso.
=> UPDATE hot SET s = 'E'; => SELECT * FROM heap_page('hot',0);
ctid | state | xmin | xmax | hhu | hot | t_ctid -------+--------+----------+-------+-----+-----+-------- (0,1) | dead | | | | | (0,2) | dead | | | | | (0,3) | dead | | | | | (0,4) | normal | 3982 (c) | 3983 | | | (0,5) (0,5) | normal | 3983 | 0 (a) | | | (0,5) (5 rows)
Todas as versões irrelevantes das linhas (0,1), (0,2) e (0,3) são limpas; depois disso, uma nova versão da linha (0,5) é adicionada ao espaço vago.
As versões das linhas restantes após a limpeza são deslocadas fisicamente para o lado dos endereços da página sênior, para que todo o espaço livre seja representado por um fragmento contínuo. Os valores dos ponteiros mudam de acordo. Graças a isso, não há problemas com a fragmentação do espaço livre na página.
Os ponteiros para versões excluídas de cadeias não podem ser liberados porque são referenciados em uma página de índice. Vejamos a primeira página do índice hot_s (porque o zero está ocupado com as meta-informações):
=> SELECT * FROM index_page('hot_s',1);
itemoffset | ctid ------------+------- 1 | (0,1) 2 | (0,2) 3 | (0,3) 4 | (0,4) 5 | (0,5) (5 rows)
Veremos a mesma imagem em outro índice:
=> SELECT * FROM index_page('hot_id',1);
itemoffset | ctid ------------+------- 1 | (0,5) 2 | (0,4) 3 | (0,3) 4 | (0,2) 5 | (0,1) (5 rows)
Você pode perceber que os ponteiros para as linhas da tabela vão aqui "para trás", mas isso não importa, porque em todas as versões das linhas o mesmo valor é id = 1. Mas no índice anterior, os ponteiros são ordenados por s valores, e isso substancialmente.
Com o acesso ao índice, o PostgreSQL pode obter (0,1), (0,2) ou (0,3) como o identificador da versão da linha. Ele tentará obter a linha correspondente da página da tabela, mas, graças ao status morto do ponteiro, ele descobrirá que essa versão não existe mais e a ignorará. (De fato, na primeira vez que detectar a falta de uma versão de uma linha da tabela, o PostgreSQL também alterará o status do ponteiro na página de índice, para que não acesse a página da tabela novamente.)
É importante que a limpeza dentro da página funcione apenas em uma página tabular e não apague as páginas de índice.
Atualizações HOT
Por que é ruim manter links para todas as versões de uma string no índice?
Primeiramente, com qualquer alteração de linha, você deve atualizar todos os índices criados para a tabela: desde que uma nova versão apareceu, você deve ter links para ela. E você precisa fazer isso em qualquer caso, mesmo que os campos que não estão incluídos no índice sejam alterados. Obviamente, isso não é muito eficaz.
Em segundo lugar, os índices acumulam links para versões históricas da string, que precisam ser limpas juntamente com as próprias versões (veremos isso mais adiante).
Além disso, há uma característica da implementação da árvore B no PostgreSQL. Se não houver espaço suficiente na página de índice para inserir uma nova linha, a página será dividida em duas e todos os dados serão redistribuídos entre elas. Isso é chamado de página dividida. No entanto, ao excluir linhas, duas páginas de índice não ficam mais "unidas" em uma. Por esse motivo, o tamanho do índice pode não diminuir, mesmo que uma parte substancial dos dados seja excluída.
Naturalmente, quanto mais índices forem criados na tabela, maiores serão as dificuldades que você terá que enfrentar.
No entanto, se o valor de uma coluna que não pertence a nenhum índice for alterado, não há sentido em criar um registro adicional na árvore B que contenha o mesmo valor de chave. É assim que a otimização funciona, chamada de atualização HOT - atualização de tupla somente de heap.
Com esta atualização, há apenas uma entrada na página de índice que se refere à primeira versão da linha na página da tabela. E já dentro desta página tabular uma cadeia de versões está organizada:
- seqüências de caracteres que são alteradas e incluídas na cadeia são marcadas com o bit Heap Hot Updated;
- as linhas que não são referenciadas no índice são marcadas com o bit Heap Only Tuple (ou seja, "apenas a versão tabular da linha");
- a vinculação regular de versões de string através do campo ctid é suportada.
Se, ao verificar um índice, o PostgreSQL entra em uma página tabular e descobre a versão marcada como Heap Hot Updated, ele entende que não precisa parar e vai além em toda a cadeia de atualização. Obviamente, para todas as versões de strings obtidas dessa maneira, a visibilidade é verificada antes de serem retornadas ao cliente.
Para examinar a operação de uma atualização HOT, exclua um índice e limpe a tabela.
=> DROP INDEX hot_s; => TRUNCATE TABLE hot;
Repita a inserção e atualize a linha.
=> INSERT INTO hot VALUES (1, 'A'); => UPDATE hot SET s = 'B';
Aqui está o que vemos na página da tabela:
=> SELECT * FROM heap_page('hot',0);
ctid | state | xmin | xmax | hhu | hot | t_ctid -------+--------+----------+-------+-----+-----+-------- (0,1) | normal | 3986 (c) | 3987 | t | | (0,2) (0,2) | normal | 3987 | 0 (a) | | t | (0,2) (2 rows)
Na página, há uma cadeia de mudanças:
- o sinalizador Heap Hot Updated indica que você precisa seguir a cadeia ctid,
- o sinalizador Heap Only Tuple indica que não há links de índice para esta versão da linha.
Com outras mudanças, a cadeia crescerá (dentro da página):
=> UPDATE hot SET s = 'C'; => UPDATE hot SET s = 'D'; => SELECT * FROM heap_page('hot',0);
ctid | state | xmin | xmax | hhu | hot | t_ctid -------+--------+----------+----------+-----+-----+-------- (0,1) | normal | 3986 (c) | 3987 (c) | t | | (0,2) (0,2) | normal | 3987 (c) | 3988 (c) | t | t | (0,3) (0,3) | normal | 3988 (c) | 3989 | t | t | (0,4) (0,4) | normal | 3989 | 0 (a) | | t | (0,4) (4 rows)
Além disso, no índice há uma única referência à "cabeça" da cadeia:
=> SELECT * FROM index_page('hot_id',1);
itemoffset | ctid ------------+------- 1 | (0,1) (1 row)
Enfatizamos que as atualizações HOT funcionam se os campos atualizados não estiverem incluídos em nenhum índice. Caso contrário, em algum índice, haveria um link diretamente para a nova versão da string, o que contradiz a ideia dessa otimização.
A otimização funciona apenas dentro dos limites de uma página; portanto, um desvio adicional da cadeia não requer acesso a outras páginas e não prejudica o desempenho.
Limpeza na página com atualizações HOT
Um caso especial, mas importante, de limpeza intra-página é a limpeza durante as atualizações HOT.
Como na última vez, já ultrapassamos o limite do fator de preenchimento, portanto, a próxima atualização deve levar a uma limpeza na página. Mas desta vez na página há uma cadeia de atualizações. A "cabeça" desta cadeia HOT deve sempre permanecer em seu lugar, pois o índice se refere a ela e o restante dos ponteiros pode ser liberado: sabe-se que eles não são referenciados de fora.
Para não tocar na "cabeça", é utilizado o endereçamento duplo: o ponteiro ao qual o índice se refere - neste caso (0,1) - recebe o status de "redirecionar", redirecionando para a versão desejada da string.
=> UPDATE hot SET s = 'E'; => SELECT * FROM heap_page('hot',0);
ctid | state | xmin | xmax | hhu | hot | t_ctid -------+---------------+----------+-------+-----+-----+-------- (0,1) | redirect to 4 | | | | | (0,2) | normal | 3990 | 0 (a) | | t | (0,2) (0,3) | unused | | | | | (0,4) | normal | 3989 (c) | 3990 | t | t | (0,2) (4 rows)
Observe que:
- versões (0,1), (0,2) e (0,3) foram apuradas,
- O ponteiro da cabeça (0,1) permaneceu, mas recebeu o status de redirecionamento,
- uma nova versão da linha é gravada no local (0.2), pois foi garantido que esta versão não possui links de índices e o ponteiro foi liberado (não utilizado).
Execute a atualização mais algumas vezes:
=> UPDATE hot SET s = 'F'; => UPDATE hot SET s = 'G'; => SELECT * FROM heap_page('hot',0);
ctid | state | xmin | xmax | hhu | hot | t_ctid -------+---------------+----------+----------+-----+-----+-------- (0,1) | redirect to 4 | | | | | (0,2) | normal | 3990 (c) | 3991 (c) | t | t | (0,3) (0,3) | normal | 3991 (c) | 3992 | t | t | (0,5) (0,4) | normal | 3989 (c) | 3990 (c) | t | t | (0,2) (0,5) | normal | 3992 | 0 (a) | | t | (0,5) (5 rows)
A atualização a seguir novamente causa a limpeza dentro da página:
=> UPDATE hot SET s = 'H'; => SELECT * FROM heap_page('hot',0);
ctid | state | xmin | xmax | hhu | hot | t_ctid -------+---------------+----------+-------+-----+-----+-------- (0,1) | redirect to 5 | | | | | (0,2) | normal | 3993 | 0 (a) | | t | (0,2) (0,3) | unused | | | | | (0,4) | unused | | | | | (0,5) | normal | 3992 (c) | 3993 | t | t | (0,2) (5 rows)
Novamente, algumas versões são limpas e o ponteiro para a "cabeça" é alterado.
Conclusão: com atualizações frequentes para colunas fora dos índices, pode fazer sentido reduzir o parâmetro fillfactor para reservar algum espaço na página para atualizações. Obviamente, devemos levar em conta que quanto menor o fator de preenchimento, mais espaço não alocado permanece na página e, consequentemente, o tamanho físico da tabela aumenta.
Quebra de corrente QUENTE
Se não houver espaço livre suficiente na página para postar uma nova versão de uma linha, a cadeia será interrompida. A versão da linha publicada em outra página precisará criar um link separado do índice.
Para obter essa situação, iniciamos uma transação paralela e construímos uma captura instantânea de dados.
| => BEGIN ISOLATION LEVEL REPEATABLE READ; | => SELECT count(*) FROM hot;
| count | ------- | 1 | (1 row)
Um instantâneo não limpa a versão das linhas na página. Agora, realizamos a atualização na primeira sessão:
=> UPDATE hot SET s = 'I'; => UPDATE hot SET s = 'J'; => UPDATE hot SET s = 'K'; => SELECT * FROM heap_page('hot',0);
ctid | state | xmin | xmax | hhu | hot | t_ctid -------+---------------+----------+----------+-----+-----+-------- (0,1) | redirect to 2 | | | | | (0,2) | normal | 3993 (c) | 3994 (c) | t | t | (0,3) (0,3) | normal | 3994 (c) | 3995 (c) | t | t | (0,4) (0,4) | normal | 3995 (c) | 3996 | t | t | (0,5) (0,5) | normal | 3996 | 0 (a) | | t | (0,5) (5 rows)
Na próxima vez que a página for atualizada, não haverá espaço suficiente na página, mas a limpeza na página não poderá liberar nada:
=> UPDATE hot SET s = 'L';
| => COMMIT;
=> SELECT * FROM heap_page('hot',0);
ctid | state | xmin | xmax | hhu | hot | t_ctid -------+---------------+----------+----------+-----+-----+-------- (0,1) | redirect to 2 | | | | | (0,2) | normal | 3993 (c) | 3994 (c) | t | t | (0,3) (0,3) | normal | 3994 (c) | 3995 (c) | t | t | (0,4) (0,4) | normal | 3995 (c) | 3996 (c) | t | t | (0,5) (0,5) | normal | 3996 (c) | 3997 | | t | (1,1) (5 rows)
Na versão (0.5), vemos um link para (1.1) que leva à página 1.
=> SELECT * FROM heap_page('hot',1);
ctid | state | xmin | xmax | hhu | hot | t_ctid -------+--------+------+-------+-----+-----+-------- (1,1) | normal | 3997 | 0 (a) | | | (1,1) (1 row)
Agora, existem duas linhas no índice, cada uma das quais aponta para o início de sua cadeia HOT:
=> SELECT * FROM index_page('hot_id',1);
itemoffset | ctid ------------+------- 1 | (1,1) 2 | (0,1) (2 rows)
Infelizmente, as informações sobre limpeza na página e atualizações HOT estão praticamente ausentes na documentação, e a verdade deve ser buscada no código-fonte. Eu recomendo começar com README.HOT .
Para ser continuado .